Transactional memory (TM TS)
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
| Cette section est incomplète |
atomic_noexcept
compound-statement
atomic_cancel
instruction-composée
atomic_commit
instruction-composée
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
| Cette section est incomplète |
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.
| Cette section est incomplète |
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é).
| Cette section est incomplète |
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
| Cette section est incomplète |
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:
-
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, opérateur new global par défaut
operator new
, opérateur delete global par défaut
operator delete
,
std::allocator::construct
si le constructeur invoqué est sûr pour les transactions,
std::allocator::destroy
si le destructeur invoqué est sûr pour les transactions,
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, chaque fonction membre non virtuelle de tous les types d'exception qui prennent en charge l'annulation de transaction (voir
atomic_cancelci-dessus)Cette section est incomplète
Raison : il y en a plus
-
std::forward
,
std::move
,
std::move_if_noexcept
,
std::align
,
std::abort
, opérateur new global par défaut
operator new
, opérateur delete global par défaut
operator delete
,
std::allocator::construct
si le constructeur invoqué est sûr pour les transactions,
std::allocator::destroy
si le destructeur invoqué est sûr pour les transactions,
std::get_temporary_buffer
,
std::return_temporary_buffer
,
std::addressof
,
std::pointer_traits::pointer_to
, chaque fonction membre non virtuelle de tous les types d'exception qui prennent en charge l'annulation de transaction (voir
-
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_cancelci-dessus)
-
chaque fonction membre virtuelle de tous les types d'exception qui prennent en charge l'annulation de transaction (voir
-
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
|
Cette section est incomplète
Motif : vérifier l'assembly avec trunk, montrer également les modifications côté appelant |
Notes
|
Cette section est incomplète
Motif : notes d'expérience du document/présentation de Wyatt |
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.