Namespaces
Variants

std:: memory_order

From cppreference.net
Concurrency support library
Threads
(C++11)
(C++20)
this_thread namespace
(C++11)
(C++11)
Cooperative cancellation
Mutual exclusion
Generic lock management
Condition variables
(C++11)
Semaphores
Latches and Barriers
(C++20)
(C++20)
Futures
(C++11)
(C++11)
(C++11)
Safe reclamation
Hazard pointers
Atomic types
(C++11)
(C++20)
Initialization of atomic types
(C++11) (deprecated in C++20)
(C++11) (deprecated in C++20)
Memory ordering
memory_order
(C++11)
(C++11) (deprecated in C++26)
Free functions for atomic operations
Free functions for atomic flags
Défini dans l'en-tête <atomic>
enum memory_order

{
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst

} ;
(depuis C++11)
(jusqu'à C++20)
enum class memory_order : /* non spécifié */

{
relaxed, consume, acquire, release, acq_rel, seq_cst
} ;
inline constexpr memory_order memory_order_relaxed = memory_order :: relaxed ;
inline constexpr memory_order memory_order_consume = memory_order :: consume ;
inline constexpr memory_order memory_order_acquire = memory_order :: acquire ;
inline constexpr memory_order memory_order_release = memory_order :: release ;
inline constexpr memory_order memory_order_acq_rel = memory_order :: acq_rel ;

inline constexpr memory_order memory_order_seq_cst = memory_order :: seq_cst ;
(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.

Table des matières

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épendance

Au 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 :

1) Cohérence écriture-écriture : Si l'évaluation A qui modifie un atomique M (une écriture) se produit avant l'évaluation B qui modifie M, alors A apparaît avant B dans l' ordre de modification de M.
2) Cohérence lecture-lecture : si une évaluation de valeur A d'un atomique M (une lecture) se produit avant une évaluation de valeur B sur M, et si la valeur de A provient d'une écriture X sur M, alors la valeur de B est soit la valeur stockée par X, soit la valeur stockée par un effet secondaire Y sur M qui apparaît après X dans l' ordre de modification de M.
3) Cohérence lecture-écriture : si une évaluation de valeur A d'un atomique M (une lecture) se produit avant une opération B sur M (une écriture), alors la valeur de A provient d'un effet secondaire (une écriture) X qui apparaît avant B dans l' ordre de modification de M.
4) Cohérence écriture-lecture : si un effet secondaire (une écriture) X sur un objet atomique M se produit avant un calcul de valeur (une lecture) B de M, alors l'évaluation B doit prendre sa valeur de X ou d'un effet secondaire Y qui suit X dans l'ordre de modification de M.

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)
2) Opérations atomiques de lecture-modification-écriture effectuées sur M par n'importe quel thread.

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 avant

Entre 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 :

1) A synchronizes-with B.
2) A est dependency-ordered before B.
3) A synchronizes-with une évaluation X, et X est sequenced-before B.
4) A est sequenced-before une certaine évaluation X, et X inter-thread happens-before B.
5) A inter-thread happens-before une évaluation X, et X inter-thread happens-before B.


Happens-before

Indé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.

Simply happens-before

Indépendamment des threads, l'évaluation A simply 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 simply happens-before X, et X simply happens-before B.

Note : sans opérations consume, les relations simply happens-before et happens-before sont identiques.

(since C++20)
(until C++26)

Happens-before

Indé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.

Note : strongly happens-before exclut les opérations de consommation.

(jusqu'en C++26)
(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 :

1) A happens-before B.
2) Il n'existe aucun autre effet secondaire X vers M où A happens-before X et X happens-before B.

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 rcu_dereference .

Voir aussi std::kill_dependency et [[ carries_dependency ]] pour un contrôle fin de la chaîne de dépendance.

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 memory_order_consume est temporairement déconseillée.

(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 memory_order_seq_cst B qui charge depuis la variable atomique M, observe l'un des éléments suivants :

  • le résultat de la dernière opération A qui a modifié M, qui apparaît avant B dans l'ordre total unique,
  • OU, s'il y avait un tel A, B peut observer le résultat d'une modification de M qui n'est pas memory_order_seq_cst et ne se produit pas avant A,
  • OU, s'il n'y avait pas un tel A, B peut observer le résultat d'une modification non liée de M qui n'est pas memory_order_seq_cst .

S'il y avait une opération memory_order_seq_cst std::atomic_thread_fence X séquencée avant B, alors B observe l'un des éléments suivants :

  • la dernière modification memory_order_seq_cst de M qui apparaît avant X dans l'ordre total unique,
  • une modification non liée de M qui apparaît plus tard dans l'ordre de modification de M.

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 memory_order_seq_cst std::atomic_thread_fence X et Y, et si A est séquencée avant X, Y est séquencée avant B, et X apparaît avant Y dans l'Ordre Total Unique, alors B observe soit :

  • l'effet de A,
  • une modification non liée de M qui apparaît après A dans l'ordre de modification de M.

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

  • il existe une memory_order_seq_cst std::atomic_thread_fence X telle que A est séquencée avant X et X apparaît avant B dans l'Ordre Total Unique,
  • ou, il existe une memory_order_seq_cst std::atomic_thread_fence Y telle que Y est séquencée avant B et A apparaît avant Y dans l'Ordre Total Unique,
  • ou, il existe des memory_order_seq_cst std::atomic_thread_fence X et Y telles que A est séquencée avant X, Y est séquencée avant B, et X apparaît avant Y dans l'Ordre Total Unique.

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 memory_order_seq_cst , y compris les barrières, qui satisfait les contraintes suivantes :

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 memory_order_acquire et memory_order_release sur certains CPU. Cela peut produire des résultats surprenants lorsque memory_order_acquire et memory_order_release sont mélangés avec memory_order_seq_cst .

Par exemple, avec x et y initialement zéro,

// 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 memory_order_seq_cst (voir Lahav et al ).

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