Namespaces
Variants

Multi-threaded executions and data races (since C++11)

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

Un thread d'exécution est un flux de contrôle au sein d'un programme qui commence par l'invocation d'une fonction de haut niveau spécifique (par std::thread , std::async , std::jthread (depuis C++20) ou d'autres moyens), et inclut récursivement chaque invocation de fonction exécutée ultérieurement par le thread.

  • Lorsqu'un thread en crée un autre, l'appel initial à la fonction de niveau supérieur du nouveau thread est exécuté par le nouveau thread, et non par le thread créateur.

Tout thread peut potentiellement accéder à n'importe quel objet et fonction dans le programme :

  • Les objets ayant une durée de stockage automatique ou locale au thread peuvent toujours être accédés par un autre thread via un pointeur ou par référence.
  • Sous une implémentation hébergée , un programme C++ peut avoir plusieurs threads s'exécutant concurremment. L'exécution de chaque thread se déroule comme défini par le reste de cette page. L'exécution du programme entier consiste en une exécution de tous ses threads.
  • Sous une implémentation autonome , il est défini par l'implémentation si un programme peut avoir plus d'un thread d'exécution.

Pour un gestionnaire de signal qui n'est pas exécuté à la suite d'un appel à std::raise , il n'est pas spécifié quel thread d'exécution contient l'invocation du gestionnaire de signal.

Table des matières

Courses de données

Différents threads d'exécution sont toujours autorisés à accéder (lire et modifier) différentes emplacements mémoire simultanément, sans interférence et sans exigences de synchronisation.

Deux expressions évaluations entrent en conflit si l'une d'elles modifie un emplacement mémoire ou commence/termine la durée de vie d'un objet dans un emplacement mémoire, et que l'autre lit ou modifie le même emplacement mémoire ou commence/termine la durée de vie d'un objet occupant un stockage qui chevauche l'emplacement mémoire.

Un programme qui a deux évaluations conflictuelles présente une data race à moins que

  • les deux évaluations s'exécutent sur le même thread ou dans le même gestionnaire de signal , ou
  • les deux évaluations conflictuelles sont des opérations atomiques (voir std::atomic ), ou
  • l'une des évaluations conflictuelles se produit-avant une autre (voir std::memory_order ).

Si une course aux données se produit, le comportement du programme est indéfini.

(En particulier, la libération d'un std::mutex est synchronisée-avec , et par conséquent, se-passe-avant l'acquisition du même mutex par un autre thread, ce qui permet d'utiliser les verrous de mutex pour se protéger contre les courses de données.)

int cnt = 0;
auto f = [&] { cnt++; };
std::thread t1{f}, t2{f}, t3{f}; // comportement indéfini
std::atomic<int> cnt{0};
auto f = [&] { cnt++; };
std::thread t1{f}, t2{f}, t3{f}; // OK
Le code C++ reste inchangé comme demandé. Le commentaire "OK" a été conservé car il fait partie du code source et ne nécessite pas de traduction dans ce contexte technique.

Courses de données des conteneurs

Tous les conteneurs de la bibliothèque standard, à l'exception de std :: vector < bool > garantissent que les modifications concurrentes du contenu de l'objet contenu dans différents éléments du même conteneur ne résulteront jamais en des courses aux données.

std::vector<int> vec = {1, 2, 3, 4};
auto f = [&](int index) { vec[index] = 5; };
std::thread t1{f, 0}, t2{f, 1}; // CORRECT
std::thread t3{f, 2}, t4{f, 2}; // comportement indéfini
std::vector<bool> vec = {false, false};
auto f = [&](int index) { vec[index] = true; };
std::thread t1{f, 0}, t2{f, 1}; // comportement indéfini

Ordre de mémoire

Lorsqu'un thread lit une valeur depuis un emplacement mémoire, il peut voir la valeur initiale, la valeur écrite dans le même thread, ou la valeur écrite dans un autre thread. Voir std::memory_order pour plus de détails sur l'ordre dans lequel les écritures effectuées par les threads deviennent visibles pour les autres threads.

Progrès avant

Absence d'obstruction

Lorsqu'un seul thread qui n'est pas bloqué dans une fonction de la bibliothèque standard exécute une fonction atomique sans verrou, cette exécution est garantie de se terminer (toutes les opérations sans verrou de la bibliothèque standard sont sans obstruction ).

Absence de verrouillage

Lorsqu'une ou plusieurs fonctions atomiques sans verrou s'exécutent simultanément, au moins l'une d'entre elles est garantie de se terminer (toutes les opérations sans verrou de la bibliothèque standard sont lock-free — il incombe à l'implémentation de garantir qu'elles ne peuvent pas être indéfiniment bloquées vivantes par d'autres threads, par exemple en volant continuellement la ligne de cache).

Garantie de progression

Dans un programme C++ valide, chaque thread finit par effectuer l'une des actions suivantes :

  • Se termine.
  • Appelle std::this_thread::yield .
  • Effectue un appel à une fonction d'E/S de bibliothèque.
  • Effectue un accès via une volatile glvalue.
  • Effectue une opération atomique ou une opération de synchronisation.
  • Poursuit l'exécution d'une boucle infinie triviale (voir ci-dessous).

Un thread est dit faire du progrès s'il exécute l'une des étapes d'exécution ci-dessus, se bloque dans une fonction de la bibliothèque standard, ou appelle une fonction atomique sans verrouillage qui ne se termine pas en raison d'un thread concurrent non bloqué.

Cela permet aux compilers de supprimer, fusionner et réorganiser toutes les boucles qui n'ont aucun comportement observable, sans avoir à prouver qu'elles finiraient par se terminer car il peut supposer qu'aucun thread d'exécution ne peut s'exécuter indéfiniment sans effectuer l'un de ces comportements observables. Une disposition est prévue pour les boucles infinies triviales, qui ne peuvent être supprimées ni réorganisées.

Boucles infinies triviales

Une instruction d'itération trivialement vide est une instruction d'itération correspondant à l'une des formes suivantes :

while ( condition ) ; (1)
while ( condition ) { } (2)
do ; while ( condition ) ; (3)
do { } while ( condition ) ; (4)
for ( init-statement condition  (optionnel) ; ) ; (5)
for ( init-statement condition  (optionnel) ; ) { } (6)
1) Une while statement dont le corps de boucle est une instruction simple vide.
2) Une while instruction dont le corps de boucle est une instruction composée vide.
3) Une do - while instruction dont le corps de boucle est une instruction simple vide.
4) Une do - while statement dont le corps de boucle est une instruction composée vide.
5) Une for instruction dont le corps de boucle est une instruction simple vide, l'instruction for n'a pas d' expression-d'itération .
6) Une for instruction dont le corps de boucle est une instruction composée vide, l'instruction for ne possède pas d' expression-d'itération .

L' expression de contrôle d'une instruction d'itération trivialement vide est :

1-4) condition .
5,6) condition si présente, sinon true .

Une boucle infinie triviale est une instruction d'itération trivialement vide pour laquelle l'expression de contrôle convertie est une expression constante , lorsqu'elle est manifestement évaluée de manière constante , et s'évalue à true .

Le corps de boucle d'une boucle infinie triviale est remplacé par un appel à la fonction std::this_thread::yield . Il est défini par l'implémentation si ce remplacement se produit sur les implémentations autonomes .

for (;;); // boucle infinie triviale, bien définie selon P2809
for (;;) { int x; } // comportement indéfini

Progression concurrente

Si un thread offre une garantie de progression concurrente , il fera des progrès (tels que définis ci-dessus) en un temps fini, tant qu'il n'a pas terminé, indépendamment du fait que d'autres threads (s'il y en a) progressent ou non.

La norme encourage, mais n'exige pas, que le thread principal et les threads démarrés par std::thread et std::jthread (depuis C++20) offrent une garantie de progression concurrente.

Progression parallèle

Si un thread offre une garantie de progression parallèle , l'implémentation n'est pas tenue de garantir que le thread finira par progresser s'il n'a pas encore exécuté d'étape d'exécution (E/S, volatile, atomique ou synchronisation), mais une fois que ce thread a exécuté une étape, il fournit les garanties de progression concurrente (cette règle décrit un thread dans un pool de threads qui exécute des tâches dans un ordre arbitraire).

Progression faiblement parallèle

Si un thread offre une garantie de progression faiblement parallèle , il ne garantit pas de progresser éventuellement, indépendamment du fait que d'autres threads progressent ou non.

De tels threads peuvent néanmoins être garantis de progresser en bloquant avec délégation de garantie de progression : si un thread P se bloque de cette manière en attendant l'achèvement d'un ensemble de threads S , alors au moins un thread dans S offrira une garantie de progression égale ou supérieure à celle de P . Une fois ce thread terminé, un autre thread dans S sera similairement renforcé. Une fois l'ensemble vide, P se débloquera.

Les algorithmes parallèles de la bibliothèque standard C++ se bloquent avec délégation de progression en attendant l'achèvement d'un ensemble non spécifié de threads gérés par la bibliothèque.

(depuis C++17)

Rapports de défauts

Les rapports de défauts modifiant le comportement suivants ont été appliqués rétroactivement aux normes C++ précédemment publiées.

DR Appliqué à Comportement tel que publié Comportement correct
CWG 1953 C++11 deux évaluations d'expression qui commencent/terminent les durées de vie
d'objets avec des stockages superposés n'étaient pas en conflit
ils sont en conflit
LWG 2200 C++11 il n'était pas clair si l'exigence de course aux données du conteneur
s'appliquait uniquement aux conteneurs de séquence
s'applique à tous les conteneurs
P2809R3 C++11 le comportement de l'exécution de boucles infinies "triviales" [1]
était indéfini
définit correctement les "boucles infinies triviales"
et a rendu le comportement bien défini
  1. « Trivial » signifie ici que l'exécution de la boucle infinie ne produit jamais aucun progrès.