Transactional memory (TM TS)
La memoria transaccional es un mecanismo de sincronización de concurrencia que combina grupos de declaraciones en transacciones, que son
- atómico (todas las sentencias ocurren, o nada ocurre)
- aislado (las sentencias en una transacción no pueden observar escrituras parciales realizadas por otra transacción, incluso si se ejecutan en paralelo)
Las implementaciones típicas utilizan memoria transaccional de hardware donde está disponible y hasta los límites en que está disponible (por ejemplo, hasta que el conjunto de cambios se satura) y recurren a memoria transaccional de software, generalmente implementada con concurrencia optimista: si otra transacción actualizó algunas de las variables utilizadas por una transacción, se reintenta silenciosamente. Por esa razón, las transacciones reintentables ("bloques atómicos") solo pueden llamar funciones transaction-safe.
Tenga en cuenta que acceder a una variable dentro de una transacción y fuera de una transacción sin otra sincronización externa es una carrera de datos.
Si las pruebas de características son compatibles, las características descritas aquí se indican mediante la constante macro __cpp_transactional_memory con un valor igual o mayor a 201505 .
Contenidos |
Bloques sincronizados
synchronized
sentencia-compuesta
Ejecuta la compound statement como si estuviera bajo un bloqueo global: todos los bloques sincronizados más externos en el programa se ejecutan en un único orden total. El final de cada bloque sincronizado se sincroniza con el comienzo del siguiente bloque sincronizado en ese orden. Los bloques sincronizados que están anidados dentro de otros bloques sincronizados no tienen semántica especial.
Los bloques sincronizados no son transacciones (a diferencia de los bloques atómicos a continuación) y pueden llamar a funciones no seguras para transacciones.
#include <iostream> #include <thread> #include <vector> int f() { static int i = 0; synchronized { // comenzar bloque sincronizado std::cout << i << " -> "; ++i; // cada llamada a f() obtiene un valor único de i std::cout << i << '\n'; return i; // finalizar bloque sincronizado } } 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(); }
Salida:
0 -> 1 1 -> 2 2 -> 3 ... 99 -> 100
Salir de un bloque sincronizado por cualquier medio (alcanzar el final, ejecutar goto, break, continue, o return, o lanzar una excepción) sale del bloque y se sincroniza-con el siguiente bloque en el orden total único si el bloque del que se salió era un bloque externo. El comportamiento es indefinido si std::longjmp se utiliza para salir de un bloque sincronizado.
No se permite ingresar a un bloque sincronizado mediante goto o switch.
Aunque los bloques sincronizados se ejecutan como si estuvieran bajo un bloqueo global, se espera que las implementaciones examinen el código dentro de cada bloque y utilicen concurrencia optimista (respaldada por memoria transaccional de hardware cuando esté disponible) para código transaccionalmente seguro y bloqueo mínimo para código no transaccionalmente seguro. Cuando un bloque sincronizado realiza una llamada a una función no inlineada, el compilador puede tener que abandonar la ejecución especulativa y mantener un bloqueo alrededor de toda la llamada a menos que la función esté declarada
transaction_safe
(ver abajo) o se utilice el atributo
[[optimize_for_synchronized]]
(ver abajo).
Bloques atómicos
| Esta sección está incompleta |
atomic_noexcept
declaración-compuesta
atomic_cancel
sentencia-compuesta
atomic_commit
declaración-compuesta
Las excepciones utilizadas para la cancelación de transacciones en
atomic_cancel
bloques son
std::bad_alloc
,
std::bad_array_new_length
,
std::bad_cast
,
std::bad_typeid
,
std::bad_exception
,
std::exception
y todas las excepciones de la biblioteca estándar derivadas de ella, y el tipo de excepción especial
std::tx_exception<T>
.
La
compound-statement
en un bloque atómico no puede ejecutar ninguna expresión o sentencia ni llamar a ninguna función que no sea
transaction_safe
(esto es un error en tiempo de compilación).
// cada llamada a f() recupera un valor único de i, incluso cuando se realiza en paralelo int f() { static int i = 0; atomic_noexcept { // comenzar transacción // printf("before %d\n", i); // error: no se puede llamar a una función no segura para transacciones ++i; return i; // confirmar transacción } }
Salir de un bloque atómico por cualquier medio que no sea una excepción (llegar al final, goto, break, continue, return) confirma la transacción. El comportamiento es indefinido si std::longjmp se utiliza para salir de un bloque atómico.
Funciones seguras para transacciones
| Esta sección está incompleta |
Una función puede declararse explícitamente como segura para transacciones utilizando la palabra clave transaction_safe en su declaración.
| Esta sección está incompleta |
En una
lambda
declaración, aparece inmediatamente después de la lista de captura, o inmediatamente después de la (palabra clave
mutable
(si se utiliza una).
| Esta sección está incompleta |
extern volatile int * p = 0; struct S { virtual ~S(); }; int f() transaction_safe { int x = 0; // ok: no es volátil p = &x; // ok: el puntero no es volátil int i = *p; // error: lectura a través de glvalue volátil S s; // error: invocación de destructor no seguro }
int f(int x) { // implícitamente transaction-safe if (x <= 0) return 0; return x + f(x - 1); }
Si una función que no es transacción-segura es llamada a través de una referencia o puntero a una función transacción-segura, el comportamiento es indefinido.
Los punteros a funciones seguras para transacciones y los punteros a funciones miembro seguras para transacciones son implícitamente convertibles a punteros a funciones y punteros a funciones miembro respectivamente. No está especificado si el puntero resultante se compara igual al original.
Funciones virtuales seguras para transacciones
| Esta sección está incompleta |
Si el final overrider de una función
transaction_safe_dynamic
no está declarado
transaction_safe
, llamarla en un bloque atómico es comportamiento indefinido.
Biblioteca estándar
Además de introducir la nueva plantilla de excepción std::tx_exception , la especificación técnica de memoria transaccional realiza los siguientes cambios en la biblioteca estándar:
-
hace que las siguientes funciones sean explícitamente
transaction_safe:
-
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, operador new global por defecto
operator new
, operador delete global por defecto
operator delete
,
std::allocator::construct
si el constructor invocado es seguro para transacciones,
std::allocator::destroy
si el destructor invocado es seguro para transacciones,
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, cada función miembro no virtual de todos los tipos de excepción que soportan cancelación de transacciones (ver
atomic_cancelarriba)Esta sección está incompleta
Razón: hay más
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, operador new global por defecto
operator new
, operador delete global por defecto
operator delete
,
std::allocator::construct
si el constructor invocado es seguro para transacciones,
std::allocator::destroy
si el destructor invocado es seguro para transacciones,
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, cada función miembro no virtual de todos los tipos de excepción que soportan cancelación de transacciones (ver
-
hace que las siguientes funciones sean explícitamente
transaction_safe_dynamic
-
-
cada función miembro virtual de todos los tipos de excepción que admiten la cancelación de transacciones (ver
atomic_cancelarriba)
-
cada función miembro virtual de todos los tipos de excepción que admiten la cancelación de transacciones (ver
-
requiere que todas las operaciones que son transacción-seguras en un
Allocator
X sean transacción-seguras en
X::rebind<>::other
Atributos
El atributo
[[
optimize_for_synchronized
]]
puede aplicarse a un declarador en una declaración de función y debe aparecer en la primera declaración de la función.
Si una función se declara
[[optimize_for_synchronized]]
en una unidad de traducción y la misma función se declara sin
[[optimize_for_synchronized]]
en otra unidad de traducción, el programa está mal formado; no se requiere diagnóstico.
Indica que una definición de función debe ser optimizada para invocación desde una synchronized statement. En particular, evita serializar bloques sincronizados que realizan una llamada a una función que es transaction-safe para la mayoría de las llamadas, pero no para todas las llamadas (por ejemplo, inserción en tabla hash que podría necesitar rehashing, asignador de memoria que podría necesitar solicitar un nuevo bloque, una función simple que raramente podría registrar logs).
std::atomic<bool> rehash{false}; // el hilo de mantenimiento ejecuta este bucle void maintenance_thread(void*) { while (!shutdown) { synchronized { if (rehash) { hash.rehash(); rehash = false; } } } } // los hilos de trabajo ejecutan cientos de miles de llamadas a esta función // cada segundo. Las llamadas a insert_key() desde bloques synchronized en otras // unidades de traducción harán que esos bloques se serialicen, a menos que insert_key() // esté marcada como [[optimize_for_synchronized]] [[optimize_for_synchronized]] void insert_key(char* key, char* value) { bool concern = hash.insert(key, value); if (concern) rehash = true; }
GCC ensamblado sin el atributo: toda la función es serializada
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
Ensamblado GCC con el atributo:
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 # Nota: este es el punto de serialización movb $1, rehash(%rip) mfence .L27: addq $8, %rsp ret
|
Esta sección está incompleta
Razón: verificar el ensamblado con trunk, también mostrar cambios en el lado del llamador |
Notas
|
Esta sección está incompleta
Razón: notas de experiencia del artículo/charla de Wyatt |
Palabras clave
atomic_cancel , atomic_commit , atomic_noexcept , synchronized , transaction_safe , transaction_safe_dynamic
Soporte del compilador
Esta especificación técnica es compatible con GCC a partir de la versión 6.1 (requiere - fgnu - tm para habilitar). Una variante anterior de esta especificación fue compatible con GCC a partir de la versión 4.7.