Multi-threaded executions and data races (since C++11)
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
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) | ||||||||
L' expression de contrôle d'une instruction d'itération trivialement vide est :
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 concurrenteSi 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èleSi 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èleSi 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
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 |
- ↑ « Trivial » signifie ici que l'exécution de la boucle infinie ne produit jamais aucun progrès.