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

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

atomic_noexcept declaración-compuesta

atomic_cancel sentencia-compuesta

atomic_commit declaración-compuesta

1) Si se lanza una excepción, std:: abort es llamado.
2) Si se lanza una excepción, std:: abort es llamado, a menos que la excepción sea una de las excepciones utilizadas para la cancelación de transacciones (ver más abajo), en cuyo caso la transacción es cancelada : los valores de todas las ubicaciones de memoria en el programa que fueron modificadas por efectos secundarios de las operaciones del bloque atómico se restauran a los valores que tenían en el momento en que se ejecutó el inicio del bloque atómico, y la excepción continúa el desenrollado de pila como es habitual.
3) Si se lanza una excepción, la transacción se confirma normalmente.

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

Una función puede declararse explícitamente como segura para transacciones utilizando la palabra clave transaction_safe en su declaración.

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).

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

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 :
  • 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_cancel arriba)
  • 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

Notas

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.