Transactional memory (TM TS)
Транзакционная память — это механизм синхронизации параллелизма, который объединяет группы операторов в транзакции, которые
- атомарность (либо выполняются все операторы, либо ничего не выполняется)
- изолированность (операторы в транзакции не могут наблюдать частично записанные изменения, сделанные другой транзакцией, даже если они выполняются параллельно)
Типичные реализации используют аппаратную транзакционную память там, где она поддерживается, и в пределах её доступности (например, до насыщения набора изменений), а в остальных случаях переходят на программную транзакционную память, обычно реализованную с помощью оптимистичной параллельности: если другая транзакция обновила некоторые переменные, используемые транзакцией, она автоматически повторяется. По этой причине повторяемые транзакции («атомарные блоки») могут вызывать только транзакционно-безопасные функции.
Обратите внимание, что доступ к переменной внутри транзакции и вне транзакции без дополнительной внешней синхронизации является гонкой данных.
Если тестирование возможностей поддерживается, возможности, описанные здесь, обозначаются макроконстантой __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
составной-оператор
Исключения, используемые для отмены транзакций в блоках
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:
-
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, глобальный по умолчанию
operator new
, глобальный по умолчанию
operator delete
,
std::allocator::construct
если вызываемый конструктор является транзакционно-безопасным,
std::allocator::destroy
если вызываемый деструктор является транзакционно-безопасным,
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, каждая невиртуальная функция-член всех типов исключений, поддерживающих отмену транзакций (см.
atomic_cancelвыше)Этот раздел не завершён
Причина: есть ещё
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, глобальный по умолчанию
operator new
, глобальный по умолчанию
operator delete
,
std::allocator::construct
если вызываемый конструктор является транзакционно-безопасным,
std::allocator::destroy
если вызываемый деструктор является транзакционно-безопасным,
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, каждая невиртуальная функция-член всех типов исключений, поддерживающих отмену транзакций (см.
-
делает следующие функции явно
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
|
Этот раздел не завершён
Причина: проверить ассемблер с trunk, также показать изменения на стороне вызывающего кода |
Примечания
|
Этот раздел не завершён
Причина: заметки из доклада/статьи Уайатта |
Ключевые слова
atomic_cancel , atomic_commit , atomic_noexcept , synchronized , transaction_safe , transaction_safe_dynamic
Поддержка компиляторами
Эта техническая спецификация поддерживается в GCC начиная с версии 6.1 (требует - fgnu - tm для активации). Более ранний вариант этой спецификации был поддержан в GCC начиная с версии 4.7.