std:: memory_order
|
Défini dans l'en-tête
<atomic>
|
||
|
enum
memory_order
{
|
(depuis C++11)
(jusqu'à C++20) |
|
|
enum
class
memory_order
:
/* non spécifié */
{
|
(depuis C++20) | |
std::memory_order
spécifie comment les accès mémoire, y compris les accès mémoire réguliers non atomiques, doivent être ordonnés autour d'une opération atomique. En l'absence de contraintes sur un système multi-cœur, lorsque plusieurs threads lisent et écrivent simultanément dans plusieurs variables, un thread peut observer les valeurs changer dans un ordre différent de celui dans lequel un autre thread les a écrites. En effet, l'ordre apparent des changements peut même différer entre plusieurs threads lecteurs. Certains effets similaires peuvent se produire même sur des systèmes uniprocesseurs en raison des transformations de compilateur autorisées par le modèle de mémoire.
Le comportement par défaut de toutes les opérations atomiques dans la bibliothèque prévoit un
ordonnancement séquentiellement cohérent
(voir la discussion ci-dessous). Ce comportement par défaut peut nuire aux performances, mais les opérations atomiques de la bibliothèque peuvent recevoir un argument supplémentaire
std::memory_order
pour spécifier les contraintes exactes, au-delà de l'atomicité, que le compilateur et le processeur doivent respecter pour cette opération.
Constantes
|
Défini dans l'en-tête
<atomic>
|
|
| Nom | Signification |
memory_order_relaxed
|
Opération relâchée : aucune contrainte de synchronisation ou d'ordonnancement n'est imposée sur les autres lectures ou écritures, seule l'atomicité de cette opération est garantie (voir Ordonnancement relâché ci-dessous). |
memory_order_consume
(obsolète en C++26) |
Une opération de chargement avec cet ordre mémoire effectue une opération de consommation sur l'emplacement mémoire concerné : aucune lecture ou écriture dans le thread courant dépendant de la valeur actuellement chargée ne peut être réordonnée avant ce chargement. Les écritures dans d'autres threads sur des variables dépendantes des données qui libèrent la même variable atomique sont visibles dans le thread courant. Sur la plupart des plateformes, cela n'affecte que les optimisations du compilateur (voir Ordonnancement libération-consommation ci-dessous). |
memory_order_acquire
|
Une opération de chargement avec cet ordre mémoire effectue l' opération d'acquisition sur l'emplacement mémoire concerné : aucune lecture ou écriture dans le thread courant ne peut être réordonnée avant ce chargement. Toutes les écritures dans d'autres threads qui libèrent la même variable atomique sont visibles dans le thread courant (voir Ordonnancement libération-acquisition ci-dessous). |
memory_order_release
|
Une opération de stockage avec cet ordre mémoire effectue l' opération de libération : aucune lecture ou écriture dans le thread courant ne peut être réordonnée après ce stockage. Toutes les écritures dans le thread courant sont visibles dans d'autres threads qui acquièrent la même variable atomique (voir Ordonnancement libération-acquisition ci-dessous) et les écritures qui portent une dépendance vers la variable atomique deviennent visibles dans d'autres threads qui consomment la même variable atomique (voir Ordonnancement libération-consommation ci-dessous). |
memory_order_acq_rel
|
Une opération de lecture-modification-écriture avec cet ordre mémoire est à la fois une opération d'acquisition et une opération de libération . Aucune lecture ou écriture mémoire dans le thread courant ne peut être réordonnée avant le chargement, ni après le stockage. Toutes les écritures dans d'autres threads qui libèrent la même variable atomique sont visibles avant la modification et la modification est visible dans d'autres threads qui acquièrent la même variable atomique. |
memory_order_seq_cst
|
Une opération de chargement avec cet ordre mémoire effectue une opération d'acquisition , un stockage effectue une opération de libération , et une lecture-modification-écriture effectue à la fois une opération d'acquisition et une opération de libération , plus un ordre total unique existe dans lequel tous les threads observent toutes les modifications dans le même ordre (voir Ordonnancement séquentiellement cohérent ci-dessous). |
Description formelle
La synchronisation inter-threads et l'ordonnancement de la mémoire déterminent comment les évaluations et les effets secondaires des expressions sont ordonnés entre différents threads d'exécution. Ils sont définis selon les termes suivants :
Séquencement-avant
Dans le même thread, l'évaluation A peut être sequenced-before l'évaluation B, comme décrit dans evaluation order .
Porte une dépendanceAu sein du même thread, l'évaluation A qui est séquencée-avant l'évaluation B peut également porter une dépendance vers B (c'est-à-dire que B dépend de A), si l'une des conditions suivantes est vraie :
1)
La valeur de A est utilisée comme opérande de B,
sauf
a)
si B est un appel à
std::kill_dependency
,
b)
si A est l'opérande gauche des opérateurs intégrés
&&
,
||
,
?:
, ou
,
.
2)
A écrit dans un objet scalaire M, B lit depuis M.
3)
A porte une dépendance vers une autre évaluation X, et X porte une dépendance vers B.
|
(jusqu'à C++26) |
Ordre de modification
Toutes les modifications apportées à une variable atomique particulière se produisent dans un ordre total spécifique à cette variable atomique unique.
Les quatre exigences suivantes sont garanties pour toutes les opérations atomiques :
Séquence de libération
Après qu'une opération de libération A est effectuée sur un objet atomique M, la plus longue sous-séquence continue de l'ordre de modification de M qui consiste en :
|
1)
Écritures effectuées par le même thread qui a effectué A.
|
(jusqu'à C++20) |
Est connu sous le nom de release sequence headed by A .
Se synchronise avec
Si un stockage atomique dans le thread A est une opération de release , un chargement atomique dans le thread B depuis la même variable est une opération d'acquire , et le chargement dans le thread B lit une valeur écrite par le stockage dans le thread A, alors le stockage dans le thread A synchronise-avec le chargement dans le thread B.
De plus, certains appels de bibliothèque peuvent être définis pour synchroniser-avec d'autres appels de bibliothèque sur d'autres threads.
Ordonné par dépendance avantEntre les threads, l'évaluation A est ordonnée par dépendance avant l'évaluation B si l'une des conditions suivantes est vraie :
1)
A effectue une
opération de release
sur un atomique M, et, dans un thread différent, B effectue une
opération de consume
sur le même atomique M, et B lit une valeur écrite
par toute partie de la séquence de release initiée
(jusqu'à C++20)
par A.
2)
A est ordonné par dépendance avant X et X transporte une dépendance vers B.
|
(jusqu'à C++26) |
Relation inter-thread happens-before
Entre les threads, l'évaluation A inter-thread happens before l'évaluation B si l'une des conditions suivantes est vraie :
Happens-beforeIndépendamment des threads, l'évaluation A happens-before l'évaluation B si l'une des conditions suivantes est vraie :
1)
A est
sequenced-before
B.
2)
A
inter-thread happens before
B.
L'implémentation doit garantir que la relation happens-before est acyclique, en introduisant une synchronisation supplémentaire si nécessaire (cela ne peut être nécessaire que si une opération consume est impliquée, voir Batty et al ). Si une évaluation modifie un emplacement mémoire, et qu'une autre lit ou modifie le même emplacement mémoire, et si au moins l'une des évaluations n'est pas une opération atomique, le comportement du programme est indéfini (le programme a une data race ) à moins qu'il n'existe une relation happens-before entre ces deux évaluations.
|
(until C++26) | ||
Happens-beforeIndépendamment des threads, l'évaluation A happens-before l'évaluation B si l'une des conditions suivantes est vraie :
1)
A est
sequenced-before
B.
2)
A
synchronizes-with
B.
3)
A
happens-before
X, et X
happens-before
B.
|
(since C++26) |
Se produit fortement avant
Indépendamment des threads, l'évaluation A se produit-avant fortement l'évaluation B si l'une des conditions suivantes est vraie :
|
1)
A est
sequenced-before
B.
2)
A
synchronizes-with
B.
3)
A
strongly happens-before
X, et X
strongly happens-before
B.
|
(jusqu'en C++20) | ||
|
1)
A est
sequenced-before
B.
2)
A
synchronizes with
B, et les deux A et B sont des opérations atomiques séquentiellement cohérentes.
3)
A est
sequenced-before
X, X
simplement
(jusqu'en C++26)
happens-before
Y, et Y est
sequenced-before
B.
4)
A
strongly happens-before
X, et X
strongly happens-before
B.
Note : informellement, si A strongly happens-before B, alors A semble être évalué avant B dans tous les contextes.
|
(depuis C++20) |
Effets secondaires visibles
L'effet secondaire A sur un scalaire M (une écriture) est visible par rapport au calcul de valeur B sur M (une lecture) si les deux conditions suivantes sont vraies :
Si l'effet secondaire A est visible par rapport au calcul de valeur B, alors le sous-ensemble contigu le plus long des effets secondaires sur M, dans l'ordre de modification , où B ne se produit pas avant celui-ci est appelé la séquence visible des effets secondaires (la valeur de M, déterminée par B, sera la valeur stockée par l'un de ces effets secondaires).
Note : la synchronisation entre les threads se résume à prévenir les courses de données (en établissant des relations de se-passe-avant) et à définir quels effets de bord deviennent visibles sous quelles conditions.
Opération de consommation
Le chargement atomique avec
memory_order_consume
ou plus fort est une opération de consommation. Notez que
std::atomic_thread_fence
impose des exigences de synchronisation plus strictes qu'une opération de consommation.
Opération d'acquisition
Le chargement atomique avec
memory_order_acquire
ou plus fort est une opération d'acquisition. L'opération
lock()
sur un
Mutex
est également une opération d'acquisition. Notez que
std::atomic_thread_fence
impose des exigences de synchronisation plus strictes qu'une opération d'acquisition.
Opération de libération
La sauvegarde atomique avec
memory_order_release
ou plus forte est une opération de libération. L'opération
unlock()
sur un
Mutex
est également une opération de libération. Notez que
std::atomic_thread_fence
impose des exigences de synchronisation plus fortes qu'une opération de libération.
Explication
Ordonnancement relâché
Les opérations atomiques étiquetées memory_order_relaxed ne sont pas des opérations de synchronisation ; elles n'imposent pas d'ordre parmi les accès mémoire concurrents. Elles garantissent uniquement l'atomicité et la cohérence de l'ordre des modifications.
Par exemple, avec x et y initialement à zéro,
// Thread 1: r1 = y.load(std::memory_order_relaxed); // A x.store(r1, std::memory_order_relaxed); // B // Thread 2: r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::memory_order_relaxed); // D
est autorisé à produire r1 == r2 == 42 car, bien que A soit séquencé avant B dans le thread 1 et C soit séquencé avant D dans le thread 2, rien n'empêche D d'apparaître avant A dans l'ordre de modification de y , et B d'apparaître avant C dans l'ordre de modification de x . L'effet secondaire de D sur y pourrait être visible par le chargement A dans le thread 1 tandis que l'effet secondaire de B sur x pourrait être visible par le chargement C dans le thread 2. En particulier, cela peut se produire si D est complété avant C dans le thread 2, soit en raison d'un réordonnancement du compilateur, soit à l'exécution.
|
Même avec un modèle de mémoire relâché, les valeurs sorties de nulle part ne sont pas autorisées à dépendre circulairement de leurs propres calculs, par exemple, avec x et y initialement à zéro, // Thread 1: r1 = y.load(std::memory_order_relaxed); if (r1 == 42) x.store(r1, std::memory_order_relaxed); // Thread 2: r2 = x.load(std::memory_order_relaxed); if (r2 == 42) y.store(42, std::memory_order_relaxed); n'est pas autorisé à produire r1 == r2 == 42 puisque le stockage de 42 dans y n'est possible que si le stockage dans x stocke 42 , ce qui dépend circulairement du stockage dans y stockant 42 . À noter qu'avant C++14, ceci était techniquement autorisé par la spécification, mais non recommandé pour les implémenteurs. |
(depuis C++14) |
L'utilisation typique de l'ordonnancement mémoire relaxé est l'incrémentation de compteurs, tels que les compteurs de référence de
std::shared_ptr
, car cela ne nécessite que l'atomicité, mais pas l'ordonnancement ou la synchronisation (notez que la décrémentation des
std::shared_ptr
compteurs nécessite une synchronisation acquérir-libérer avec le destructeur).
#include <atomic> #include <iostream> #include <thread> #include <vector> std::atomic<int> cnt = {0}; void f() { for (int n = 0; n < 1000; ++n) cnt.fetch_add(1, std::memory_order_relaxed); } int main() { std::vector<std::thread> v; for (int n = 0; n < 10; ++n) v.emplace_back(f); for (auto& t : v) t.join(); std::cout << "Final counter value is " << cnt << '\n'; }
Sortie :
Final counter value is 10000
Ordonnancement Release-Acquire
Si un stockage atomique dans le thread A est étiqueté memory_order_release , un chargement atomique dans le thread B depuis la même variable est étiqueté memory_order_acquire , et que le chargement dans le thread B lit une valeur écrite par le stockage dans le thread A, alors le stockage dans le thread A synchronise-avec le chargement dans le thread B.
Toutes les écritures en mémoire (y compris non atomiques et atomiques relaxées) qui se sont produites happened-before le stockage atomique du point de vue du thread A, deviennent des visible side-effects dans le thread B. Autrement dit, une fois le chargement atomique terminé, le thread B est garanti de voir tout ce que le thread A a écrit en mémoire. Cette promesse ne tient que si B retourne effectivement la valeur stockée par A, ou une valeur ultérieure dans la séquence de libération.
La synchronisation n'est établie qu'entre les threads qui libèrent et acquièrent la même variable atomique. D'autres threads peuvent voir un ordre différent des accès mémoire que celui perçu par l'un ou l'autre des threads synchronisés, ou par les deux.
Sur les systèmes fortement ordonnés — x86, SPARC TSO, mainframe IBM, etc. — l'ordonnancement release-acquire est automatique pour la majorité des opérations. Aucune instruction CPU supplémentaire n'est émise pour ce mode de synchronisation ; seules certaines optimisations du compilateur sont affectées (par exemple, le compilateur est empêché de déplacer les stockages non atomiques au-delà du stockage atomique release ou d'effectuer des chargements non atomiques avant le chargement atomique acquire). Sur les systèmes faiblement ordonnés (ARM, Itanium, PowerPC), des instructions CPU spéciales de chargement ou de barrière mémoire sont utilisées.
Les verrous d'exclusion mutuelle, tels que std::mutex ou le spinlock atomique , sont un exemple de synchronisation acquérir-relâcher : lorsque le verrou est relâché par le thread A et acquis par le thread B, tout ce qui s'est produit dans la section critique (avant le relâchement) dans le contexte du thread A doit être visible par le thread B (après l'acquisition) qui exécute la même section critique.
#include <atomic> #include <cassert> #include <string> #include <thread> std::atomic<std::string*> ptr; int data; void producer() { std::string* p = new std::string("Hello"); data = 42; ptr.store(p, std::memory_order_release); } void consumer() { std::string* p2; while (!(p2 = ptr.load(std::memory_order_acquire))) ; assert(*p2 == "Hello"); // ne se déclenche jamais assert(data == 42); // ne se déclenche jamais } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); }
L'exemple suivant démontre l'ordonnancement transitive release-acquire à travers trois threads, en utilisant une séquence de release.
#include <atomic> #include <cassert> #include <thread> #include <vector> std::vector<int> data; std::atomic<int> flag = {0}; void thread_1() { data.push_back(42); flag.store(1, std::memory_order_release); } void thread_2() { int expected = 1; // memory_order_relaxed is okay because this is an RMW, // and RMWs (with any ordering) following a release form a release sequence while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) { expected = 1; } } void thread_3() { while (flag.load(std::memory_order_acquire) < 2) ; // if we read the value 2 from the atomic flag, we see 42 in the vector assert(data.at(0) == 42); // will never fire } int main() { std::thread a(thread_1); std::thread b(thread_2); std::thread c(thread_3); a.join(); b.join(); c.join(); }
Ordonnancement Release-Consume
|
Si un stockage atomique dans le thread A est étiqueté memory_order_release , un chargement atomique dans le thread B de la même variable est étiqueté memory_order_consume , et le chargement dans le thread B lit une valeur écrite par le stockage dans le thread A, alors le stockage dans le thread A est ordonné par dépendance avant le chargement dans le thread B. Toutes les écritures en mémoire (non atomiques et atomiques relâchées) qui se sont produites avant le stockage atomique du point de vue du thread A, deviennent des effets secondaires visibles dans ces opérations du thread B dans lesquelles l'opération de chargement transporte la dépendance , c'est-à-dire qu'une fois le chargement atomique terminé, ces opérateurs et fonctions dans le thread B qui utilisent la valeur obtenue du chargement sont garantis de voir ce que le thread A a écrit en mémoire. La synchronisation est établie uniquement entre les threads relâchant et consommant la même variable atomique. D'autres threads peuvent voir un ordre différent des accès mémoire que l'un ou les deux threads synchronisés. Sur tous les CPU grand public autres que DEC Alpha, l'ordonnancement par dépendance est automatique, aucune instruction CPU supplémentaire n'est émise pour ce mode de synchronisation, seules certaines optimisations du compilateur sont affectées (par exemple, le compilateur est empêché d'effectuer des chargements spéculatifs sur les objets impliqués dans la chaîne de dépendance).
Les cas d'utilisation typiques de cet ordonnancement impliquent l'accès en lecture à des structures de données concurrentes rarement modifiées (tables de routage, configuration, politiques de sécurité, règles de pare-feu, etc.) et les situations d'éditeur-abonné avec publication médiée par pointeur, c'est-à-dire lorsque le producteur publie un pointeur via lequel le consommateur peut accéder à l'information : il n'est pas nécessaire de rendre visible au consommateur tout ce que le producteur a écrit en mémoire (ce qui peut être une opération coûteuse sur les architectures faiblement ordonnées). Un exemple d'un tel scénario est
Voir aussi
std::kill_dependency
et
Notez qu'actuellement (2/2015) aucun compilateur de production connu ne suit les chaînes de dépendance : les opérations de consommation sont élevées en opérations d'acquisition. |
(jusqu'à C++26) |
|
La spécification de l'ordonnancement release-consume est en cours de révision, et l'utilisation de
|
(depuis C++17)
(jusqu'à C++26) |
|
L'ordonnancement release-consume a le même effet que l'ordonnancement release-acquire et est déprécié. |
(depuis C++26) |
Cet exemple démontre la synchronisation par ordre de dépendance pour la publication via un pointeur : l'entier data n'est pas lié au pointeur vers la chaîne par une relation de dépendance de données, donc sa valeur est indéfinie chez le consommateur.
#include <atomic> #include <cassert> #include <string> #include <thread> std::atomic<std::string*> ptr; int data; void producer() { std::string* p = new std::string("Hello"); data = 42; ptr.store(p, std::memory_order_release); } void consumer() { std::string* p2; while (!(p2 = ptr.load(std::memory_order_consume))) ; assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr assert(data == 42); // may or may not fire: data does not carry dependency from ptr } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); }
Ordonnancement séquentiellement cohérent
Les opérations atomiques étiquetées memory_order_seq_cst ordonnent la mémoire non seulement de la même manière que l'ordonnancement release/acquire (tout ce qui s'est produit-avant un stockage dans un thread devient un effet secondaire visible dans le thread qui a effectué un chargement), mais établissent également un ordre de modification total unique de toutes les opérations atomiques ainsi étiquetées.
|
Formellement,
chaque opération
S'il y avait une opération
Pour une paire d'opérations atomiques sur M appelées A et B, où A écrit et B lit la valeur de M, s'il y a deux
Pour une paire de modifications atomiques de M appelées A et B, B se produit après A dans l'ordre de modification de M si
Notez que cela signifie que :
1)
dès que des opérations atomiques qui ne sont pas étiquetées
memory_order_seq_cst
entrent en jeu, la cohérence séquentielle est perdue,
2)
les barrières séquentiellement cohérentes n'établissent un ordre total que pour les barrières elles-mêmes, pas pour les opérations atomiques en général (
séquencée avant
n'est pas une relation inter-threads, contrairement à
se produit avant
).
|
(jusqu'à C++20) |
|
Formellement,
une opération atomique A sur un objet atomique M est ordonnée-par-cohérence-avant une autre opération atomique B sur M si l'une des conditions suivantes est vraie :
1)
A est une modification, et B lit la valeur stockée par A,
2)
A précède B dans l'
ordre de modification
de M,
3)
A lit la valeur stockée par une modification atomique X, X précède B dans l'
ordre de modification
, et A et B ne sont pas la même opération atomique de lecture-modification-écriture,
4)
A est
ordonnée-par-cohérence-avant
X, et X est
ordonnée-par-cohérence-avant
B.
Il existe un ordre total unique S sur toutes les opérations
1)
si A et B sont des opérations
memory_order_seq_cst
, et A
se-produit-fortement-avant
B, alors A précède B dans S,
2)
pour chaque paire d'opérations atomiques A et B sur un objet M, où A est
ordonnée-par-cohérence-avant
B :
a)
si A et B sont toutes deux des opérations
memory_order_seq_cst
, alors A précède B dans S,
b)
si A est une opération
memory_order_seq_cst
, et B
se-produit-avant
une barrière
memory_order_seq_cst
Y, alors A précède Y dans S,
c)
si une barrière
memory_order_seq_cst
X
se-produit-avant
A, et B est une opération
memory_order_seq_cst
, alors X précède B dans S,
d)
si une barrière
memory_order_seq_cst
X
se-produit-avant
A, et B
se-produit-avant
une barrière
memory_order_seq_cst
Y, alors X précède Y dans S.
La définition formelle garantit que :
1)
l'ordre total unique est cohérent avec l'
ordre de modification
de tout objet atomique,
2)
un chargement
memory_order_seq_cst
obtient sa valeur soit de la dernière modification
memory_order_seq_cst
, soit d'une modification non-
memory_order_seq_cst
qui ne
se-produit-pas-avant
les modifications
memory_order_seq_cst
précédentes.
L'ordre total unique peut ne pas être cohérent avec
se-produit-avant
. Cela permet une implémentation plus efficace de
Par exemple, avec
// Thread 1: x.store(1, std::memory_order_seq_cst); // A y.store(1, std::memory_order_release); // B // Thread 2: r1 = y.fetch_add(1, std::memory_order_seq_cst); // C r2 = y.load(std::memory_order_relaxed); // D // Thread 3: y.store(3, std::memory_order_seq_cst); // E r3 = x.load(std::memory_order_seq_cst); // F
est autorisé à produire
r1
==
1
&&
r2
==
3
&&
r3
==
0
, où A
se-produit-avant
C, mais C précède A dans l'ordre total unique C-E-F-A des opérations
Notez que :
1)
dès que des opérations atomiques non étiquetées
memory_order_seq_cst
entrent en jeu, la garantie de cohérence séquentielle pour le programme est perdue,
2)
dans de nombreux cas, les opérations atomiques
memory_order_seq_cst
peuvent être réordonnancées
par rapport aux autres opérations atomiques effectuées par le même thread.
|
(depuis C++20) |
L'ordonnancement séquentiel peut être nécessaire dans les situations de producteurs multiples-consommateurs multiples où tous les consommateurs doivent observer les actions de tous les producteurs se produisant dans le même ordre.
L'ordonnancement séquentiel total nécessite une instruction de barrière mémoire complète du CPU sur tous les systèmes multi-cœurs. Cela peut devenir un goulot d'étranglement de performance car il force les accès mémoire concernés à se propager vers chaque cœur.
Cet exemple démontre une situation où un ordre séquentiel est nécessaire. Tout autre ordre pourrait déclencher l'assertion car il serait possible pour les threads
c
et
d
d'observer les changements des atomiques
x
et
y
dans un ordre opposé.
#include <atomic> #include <cassert> #include <thread> std::atomic<bool> x = {false}; std::atomic<bool> y = {false}; std::atomic<int> z = {0}; void write_x() { x.store(true, std::memory_order_seq_cst); } void write_y() { y.store(true, std::memory_order_seq_cst); } void read_x_then_y() { while (!x.load(std::memory_order_seq_cst)) ; if (y.load(std::memory_order_seq_cst)) ++z; } void read_y_then_x() { while (!y.load(std::memory_order_seq_cst)) ; if (x.load(std::memory_order_seq_cst)) ++z; } int main() { std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); a.join(); b.join(); c.join(); d.join(); assert(z.load() != 0); // will never happen }
Relation avec volatile
Dans un thread d'exécution, les accès (lectures et écritures) via des glvalues volatiles ne peuvent pas être réordonnés au-delà des effets de bord observables (y compris d'autres accès volatiles) qui sont séquencés-avant ou séquencés-après dans le même thread, mais cet ordre n'est pas garanti d'être observé par un autre thread, car l'accès volatile n'établit pas de synchronisation inter-threads.
De plus, les accès volatiles ne sont pas atomiques (une lecture et une écriture concurrentes constituent une data race ) et n'ordonnancent pas la mémoire (les accès mémoire non volatiles peuvent être librement réordonnés autour de l'accès volatile).
Une exception notable est Visual Studio, où, avec les paramètres par défaut, chaque écriture volatile a une sémantique de release et chaque lecture volatile a une sémantique d'acquire (
Microsoft Docs
), et ainsi les volatiles peuvent être utilisés pour la synchronisation inter-threads. Les sémantiques standard du
volatile
ne sont pas applicables à la programmation multithread, bien qu'elles soient suffisantes pour, par exemple, la communication avec un gestionnaire de
std::signal
qui s'exécute dans le même thread lorsqu'appliquées aux variables
sig_atomic_t
. L'option du compilateur
/volatile:iso
peut être utilisée pour restaurer un comportement conforme à la norme, ce qui est le paramètre par défaut lorsque la plateforme cible est ARM.
Voir aussi
|
Documentation C
pour
memory order
|
Liens externes
| 1. | Protocole MOESI |
| 2. | x86-TSO : Un modèle de programmeur rigoureux et utilisable pour les multiprocesseurs x86 P. Sewell et al., 2010 |
| 3. | Introduction tutorielle aux modèles de mémoire relâchés ARM et POWER P. Sewell et al., 2012 |
| 4. | MESIF : Un protocole de cohérence de cache à deux sauts pour les interconnexions point à point J.R. Goodman, H.H.J. Hum, 2009 |
| 5. | Modèles de mémoire Russ Cox, 2021 |
|
Cette section est incomplète
Motif : Trouvons de bonnes références sur QPI, MOESI, et peut-être Dragon. |