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 mémoire transactionnelle est un mécanisme de synchronisation concurrentielle qui regroupe des instructions en transactions, qui sont

  • atomique (soit toutes les instructions s'exécutent, soit aucune ne s'exécute)
  • isolé (les instructions d'une transaction ne peuvent pas observer des écritures partielles effectuées par une autre transaction, même si elles s'exécutent en parallèle)

Les implémentations typiques utilisent la mémoire transactionnelle matérielle lorsqu'elle est prise en charge et dans les limites de sa disponibilité (par exemple, jusqu'à la saturation du jeu de modifications) et recourent à la mémoire transactionnelle logicielle, généralement mise en œuvre avec un contrôle de concurrence optimiste : si une autre transaction a mis à jour certaines des variables utilisées par une transaction, celle-ci est silencieusement réessayée. Pour cette raison, les transactions refaçonnables ("blocs atomiques") ne peuvent appeler que des fonctions sûres pour les transactions.

Notez que l'accès à une variable dans une transaction et en dehors d'une transaction sans autre synchronisation externe constitue une course aux données.

Si le test de fonctionnalité est pris en charge, les fonctionnalités décrites ici sont indiquées par la constante macro __cpp_transactional_memory avec une valeur égale ou supérieure à 201505 .

Table des matières

Blocs synchronisés

synchronized instruction-composée

Exécute le compound statement comme s'il était sous un verrou global : tous les blocs synchronisés les plus externes du programme s'exécutent dans un ordre total unique. La fin de chaque bloc synchronisé se synchronise avec le début du bloc synchronisé suivant dans cet ordre. Les blocs synchronisés imbriqués dans d'autres blocs synchronisés n'ont pas de sémantique particulière.

Les blocs synchronisés ne sont pas des transactions (contrairement aux blocs atomiques ci-dessous) et peuvent appeler des fonctions non sécurisées pour les transactions.

#include <iostream>
#include <thread>
#include <vector>
int f()
{
    static int i = 0;
    synchronized { // début du bloc synchronisé
        std::cout << i << " -> ";
        ++i;       // chaque appel à f() obtient une valeur unique de i
        std::cout << i << '\n';
        return i;  // fin du bloc synchronisé
    }
}
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();
}

Sortie :

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

Quitter un bloc synchronisé par n'importe quel moyen (atteindre la fin, exécuter goto, break, continue, ou return, ou lever une exception) sort du bloc et se synchronise-avec le bloc suivant dans l'ordre total unique si le bloc quitté était un bloc externe. Le comportement est indéfini si std::longjmp est utilisé pour quitter un bloc synchronisé.

L'entrée dans un bloc synchronisé par goto ou switch n'est pas autorisée.

Bien que les blocs synchronisés s'exécutent comme s'ils étaient sous un verrouillage global, les implémentations sont censées examiner le code dans chaque bloc et utiliser la concurrence optimiste (soutenue par la mémoire transactionnelle matérielle lorsque disponible) pour le code sûr pour les transactions et un verrouillage minimal pour le code non sûr pour les transactions. Lorsqu'un bloc synchronisé effectue un appel à une fonction non intégrée, le compilateur peut devoir abandonner l'exécution spéculative et maintenir un verrouillage autour de l'appel entier, sauf si la fonction est déclarée transaction_safe (voir ci-dessous) ou si l'attribut [[optimize_for_synchronized]] (voir ci-dessous) est utilisé.

Blocs atomiques

atomic_noexcept compound-statement

atomic_cancel instruction-composée

atomic_commit instruction-composée

1) Si une exception est levée, std:: abort est appelé.
2) Si une exception est levée, std:: abort est appelée, sauf si l'exception est l'une des exceptions utilisées pour l'annulation de transaction (voir ci-dessous), auquel cas la transaction est annulée : les valeurs de tous les emplacements mémoire dans le programme qui ont été modifiés par les effets secondaires des opérations du bloc atomique sont restaurées aux valeurs qu'ils avaient au moment où le début du bloc atomique a été exécuté, et l'exception continue le déroulement de la pile comme d'habitude.
3) Si une exception est levée, la transaction est validée normalement.

Les exceptions utilisées pour l'annulation de transaction dans les blocs atomic_cancel sont std::bad_alloc , std::bad_array_new_length , std::bad_cast , std::bad_typeid , std::bad_exception , std::exception et toutes les exceptions de la bibliothèque standard qui en dérivent, ainsi que le type d'exception spécial std::tx_exception<T> .

La compound-statement dans un bloc atomique n'est pas autorisée à exécuter toute expression ou instruction ou appeler toute fonction qui n'est pas transaction_safe (ceci est une erreur de compilation).

// chaque appel à f() récupère une valeur unique de i, même en parallèle
int f()
{
    static int i = 0;
    atomic_noexcept { // début de la transaction
//  printf("before %d\n", i); // erreur : impossible d'appeler une fonction non transaction-safe
        ++i;
        return i; // validation de la transaction
    }
}

Quitter un bloc atomique par tout moyen autre qu'une exception (atteindre la fin, goto, break, continue, return) valide la transaction. Le comportement est indéfini si std::longjmp est utilisé pour sortir d'un bloc atomique.

Fonctions sûres pour les transactions

Une fonction peut être explicitement déclarée comme sûre pour les transactions en utilisant le mot-clé transaction_safe dans sa déclaration.

Dans une lambda , il apparaît soit immédiatement après la liste de capture, soit immédiatement après le (mot-clé mutable (s'il est utilisé).

extern volatile int * p = 0;
struct S
{
    virtual ~S();
};
int f() transaction_safe
{
    int x = 0;  // ok : non volatile
    p = &x;     // ok : le pointeur n'est pas volatile
    int i = *p; // erreur : lecture via une glvalue volatile
    S s;        // erreur : invocation d'un destructeur non sécurisé
}
int f(int x) { // implicitement transaction-safe
    if (x <= 0)
        return 0;
    return x + f(x - 1);
}

Si une fonction qui n'est pas transaction-sûre est appelée via une référence ou un pointeur vers une fonction transaction-sûre, le comportement est indéfini.


Les pointeurs vers des fonctions sûres pour les transactions et les pointeurs vers des fonctions membres sûres pour les transactions sont implicitement convertibles en pointeurs vers des fonctions et en pointeurs vers des fonctions membres respectivement. Il n'est pas spécifié si le pointeur résultant est égal à l'original lors de la comparaison.

Fonctions virtuelles sûres en transaction

Si le remplacement final d'une fonction transaction_safe_dynamic n'est pas déclaré transaction_safe , l'appeler dans un bloc atomique est un comportement indéfini.

Bibliothèque standard

En plus de présenter le nouveau modèle d'exception std::tx_exception , la spécification technique de la mémoire transactionnelle apporte les modifications suivantes à la bibliothèque standard :

  • rend les fonctions suivantes explicitement transaction_safe :
  • rend les fonctions suivantes explicitement transaction_safe_dynamic
  • chaque fonction membre virtuelle de tous les types d'exception qui prennent en charge l'annulation de transaction (voir atomic_cancel ci-dessus)
  • exige que toutes les opérations qui sont sûres en transaction sur un Allocator X soient sûres en transaction sur X::rebind<>::other

Attributs

L'attribut [[ optimize_for_synchronized ]] peut être appliqué à un déclarateur dans une déclaration de fonction et doit apparaître sur la première déclaration de la fonction.

Si une fonction est déclarée [[optimize_for_synchronized]] dans une unité de traduction et que la même fonction est déclarée sans [[optimize_for_synchronized]] dans une autre unité de traduction, le programme est mal formé ; aucun diagnostic requis.

Cela indique qu'une définition de fonction devrait être optimisée pour être invoquée depuis une instruction synchronized . En particulier, elle évite de sérialiser les blocs synchronisés qui effectuent un appel à une fonction qui est transactionnellement sûre pour la majorité des appels, mais pas pour tous les appels (par exemple, l'insertion dans une table de hachage qui pourrait nécessiter un rehachage, un allocateur qui pourrait devoir demander un nouveau bloc, une fonction simple qui pourrait rarement journaliser).

std::atomic<bool> rehash{false};
// le thread de maintenance exécute cette boucle
void maintenance_thread(void*)
{
    while (!shutdown)
    {
        synchronized
        {
            if (rehash)
            {
                hash.rehash();
                rehash = false;
            }
        }
    }
}
// les threads de travail exécutent des centaines de milliers d'appels à cette fonction
// chaque seconde. Les appels à insert_key() depuis des blocs synchronisés dans d'autres
// unités de traduction entraîneront la sérialisation de ces blocs, sauf si insert_key()
// est marquée [[optimize_for_synchronized]]
[[optimize_for_synchronized]] void insert_key(char* key, char* value)
{
    bool concern = hash.insert(key, value);
    if (concern)
        rehash = true;
}

GCC assembly sans l'attribut : la fonction entière est sérialisée

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 assembleur avec l'attribut :

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 # Note: this is the serialization point
	movb	$1, rehash(%rip)
	mfence
.L27:
	addq	$8, %rsp
	ret

Notes

Mots-clés

atomic_cancel , atomic_commit , atomic_noexcept , synchronized , transaction_safe , transaction_safe_dynamic

Support du compilateur

Cette spécification technique est prise en charge par GCC à partir de la version 6.1 (nécessite - fgnu - tm pour l'activer). Une variante antérieure de cette spécification était prise en charge dans GCC à partir de la version 4.7.