Namespaces
Variants

Transactional memory (TM TS)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Транзакционная память — это механизм синхронизации параллелизма, который объединяет группы операторов в транзакции, которые

  • атомарность (либо выполняются все операторы, либо ничего не выполняется)
  • изолированность (операторы в транзакции не могут наблюдать частично записанные изменения, сделанные другой транзакцией, даже если они выполняются параллельно)

Типичные реализации используют аппаратную транзакционную память там, где она поддерживается, и в пределах её доступности (например, до насыщения набора изменений), а в остальных случаях переходят на программную транзакционную память, обычно реализованную с помощью оптимистичной параллельности: если другая транзакция обновила некоторые переменные, используемые транзакцией, она автоматически повторяется. По этой причине повторяемые транзакции («атомарные блоки») могут вызывать только транзакционно-безопасные функции.

Обратите внимание, что доступ к переменной внутри транзакции и вне транзакции без дополнительной внешней синхронизации является гонкой данных.

Если тестирование возможностей поддерживается, возможности, описанные здесь, обозначаются макроконстантой __cpp_transactional_memory со значением равным или большим 201505 .

Содержание

Синхронизированные блоки

synchronized составной-оператор

Выполняет составной оператор как будто под глобальной блокировкой: все внешние синхронизированные блоки в программе выполняются в едином общем порядке. Конец каждого синхронизированного блока синхронизируется с началом следующего синхронизированного блока в этом порядке. Синхронизированные блоки, вложенные в другие синхронизированные блоки, не имеют специальной семантики.

Синхронизированные блоки не являются транзакциями (в отличие от атомарных блоков ниже) и могут вызывать функции, небезопасные для транзакций.

#include <iostream>
#include <thread>
#include <vector>
int f()
{
    static int i = 0;
    synchronized { // начало синхронизированного блока
        std::cout << i << " -> ";
        ++i;       // каждый вызов f() получает уникальное значение i
        std::cout << i << '\n';
        return i;  // конец синхронизированного блока
    }
}
int main()
{
    std::vector<std::thread> v(10);
    for (auto& t : v)
        t = std::thread([] { for (int n = 0; n < 10; ++n) f(); });
    for (auto& t : v)
        t.join();
}

Вывод:

0 -> 1
1 -> 2
2 -> 3
...
99 -> 100

Выход из синхронизированного блока любым способом (достижение конца, выполнение goto, break, continue или return, либо выбрасывание исключения) завершает блок и синхронизируется-с следующим блоком в едином общем порядке, если завершенный блок был внешним блоком. Поведение не определено, если std::longjmp используется для выхода из синхронизированного блока.

Вход в синхронизированный блок с помощью goto или switch не допускается.

Хотя синхронизированные блоки выполняются как будто под глобальной блокировкой, ожидается, что реализации будут анализировать код внутри каждого блока и использовать оптимистичную параллельность (поддерживаемую аппаратной транзакционной памятью, где доступно) для транзакционно-безопасного кода и минимальные блокировки для нетранзакционно-безопасного кода. Когда синхронизированный блок вызывает невстроенную функцию, компилятору, возможно, придется выйти из спекулятивного выполнения и удерживать блокировку на протяжении всего вызова, если только функция не объявлена transaction_safe (см. ниже) или не используется атрибут [[optimize_for_synchronized]] (см. ниже).

Атомарные блоки

atomic_noexcept составной-оператор

atomic_cancel составной-оператор

atomic_commit составной-оператор

1) Если возникает исключение, std:: abort вызывается.
2) Если возникает исключение, std:: abort вызывается, за исключением случаев, когда исключение является одним из исключений, используемых для отмены транзакции (см. ниже), в этом случае транзакция отменяется : значения всех ячеек памяти в программе, которые были изменены побочными эффектами операций атомарного блока, восстанавливаются к значениям, которые они имели на момент начала выполнения атомарного блока, и исключение продолжает раскрутку стека как обычно.
3) Если возникает исключение, транзакция фиксируется обычным образом.

Исключения, используемые для отмены транзакций в блоках atomic_cancel , это std::bad_alloc , std::bad_array_new_length , std::bad_cast , std::bad_typeid , std::bad_exception , std::exception и все исключения стандартной библиотеки, производные от него, а также специальный тип исключения std::tx_exception<T> .

Составной оператор compound-statement в атомарном блоке не может выполнять любое выражение, оператор или вызывать любую функцию, которая не является transaction_safe (это ошибка времени компиляции).

// каждый вызов f() возвращает уникальное значение i, даже при выполнении в параллельном режиме
int f()
{
    static int i = 0;
    atomic_noexcept { // начало транзакции
//  printf("before %d\n", i); // ошибка: вызов небезопасной для транзакций функции запрещен
        ++i;
        return i; // фиксация транзакции
    }
}

Выход из атомарного блока любым способом, кроме исключения (достижение конца, goto, break, continue, return), фиксирует транзакцию. Поведение не определено, если std::longjmp используется для выхода из атомарного блока.

Транзакционно-безопасные функции

Функция может быть явно объявлена транзакционно-безопасной с использованием ключевого слова transaction_safe в её объявлении.

В объявлении лямбда-выражения оно появляется либо сразу после списка захвата, либо сразу после (ключевого слова mutable (если оно используется).

extern volatile int * p = 0;
struct S
{
    virtual ~S();
};
int f() transaction_safe
{
    int x = 0;  // ок: не volatile
    p = &x;     // ок: указатель не volatile
    int i = *p; // ошибка: чтение через volatile glvalue
    S s;        // ошибка: вызов небезопасного деструктора
}
int f(int x) { // неявно транзакционно-безопасная
    if (x <= 0)
        return 0;
    return x + f(x - 1);
}

Если функция, не являющаяся транзакционно-безопасной, вызывается через ссылку или указатель на транзакционно-безопасную функцию, поведение не определено.


Указатели на транзакционно-безопасные функции и указатели на транзакционно-безопасные функции-члены неявно преобразуемы в указатели на функции и указатели на функции-члены соответственно. Не определено, будет ли результирующий указатель сравниваться как равный с исходным.

Транзакционно-безопасные виртуальные функции

Если конечный переопределитель функции transaction_safe_dynamic не объявлен как transaction_safe , её вызов в атомарном блоке является неопределённым поведением.

Стандартная библиотека

Помимо введения нового шаблона исключения std::tx_exception , техническая спецификация транзакционной памяти вносит следующие изменения в стандартную библиотеку:

  • делает следующие функции явно transaction_safe :
  • делает следующие функции явно transaction_safe_dynamic
  • каждая виртуальная функция-член всех типов исключений, поддерживающих отмену транзакции (см. atomic_cancel выше)
  • требует, чтобы все операции, которые являются транзакционно-безопасными для Allocator X, оставались транзакционно-безопасными для X::rebind<>::other

Атрибуты

Атрибут [[ optimize_for_synchronized ]] может применяться к декларатору в объявлении функции и должен присутствовать при первом объявлении функции.

Если функция объявлена [[optimize_for_synchronized]] в одной единице трансляции и та же функция объявлена без [[optimize_for_synchronized]] в другой единице трансляции, программа является некорректной; диагностика не требуется.

Указывает, что определение функции должно быть оптимизировано для вызова из synchronized оператора. В частности, это позволяет избежать сериализации синхронизированных блоков, которые вызывают функцию, являющуюся транзакционно-безопасной для большинства вызовов, но не для всех (например, вставка в хэш-таблицу, которая может потребовать рехэширования, аллокатор, который может запросить новый блок, простая функция, которая может редко выполнять логирование).

std::atomic<bool> rehash{false};
// поток обслуживания выполняет этот цикл
void maintenance_thread(void*)
{
    while (!shutdown)
    {
        synchronized
        {
            if (rehash)
            {
                hash.rehash();
                rehash = false;
            }
        }
    }
}
// рабочие потоки выполняют сотни тысяч вызовов этой функции
// каждую секунду. Вызовы insert_key() из synchronized блоков в других
// единицах трансляции приведут к сериализации этих блоков, если insert_key()
// не помечена [[optimize_for_synchronized]]
[[optimize_for_synchronized]] void insert_key(char* key, char* value)
{
    bool concern = hash.insert(key, value);
    if (concern)
        rehash = true;
}

GCC ассемблер без атрибута: вся функция сериализуется

insert_key(char*, char*):
	subq	$8, %rsp
	movq	%rsi, %rdx
	movq	%rdi, %rsi
	movl	$hash, %edi
	call	Hash::insert(char*, char*)
	testb	%al, %al
	je	.L20
	movb	$1, rehash(%rip)
	mfence
.L20:
	addq	$8, %rsp
	ret

GCC ассемблер с атрибутом:

transaction clone for insert_key(char*, char*):
	subq	$8, %rsp
	movq	%rsi, %rdx
	movq	%rdi, %rsi
	movl	$hash, %edi
	call	transaction clone for Hash::insert(char*, char*)
	testb	%al, %al
	je	.L27
	xorl	%edi, %edi
	call	_ITM_changeTransactionMode # Примечание: это точка сериализации
	movb	$1, rehash(%rip)
	mfence
.L27:
	addq	$8, %rsp
	ret

Примечания

Ключевые слова

atomic_cancel , atomic_commit , atomic_noexcept , synchronized , transaction_safe , transaction_safe_dynamic

Поддержка компиляторами

Эта техническая спецификация поддерживается в GCC начиная с версии 6.1 (требует - fgnu - tm для активации). Более ранний вариант этой спецификации был поддержан в GCC начиная с версии 4.7.