Namespaces
Variants

memory_order

From cppreference.net
Défini dans l'en-tête <stdatomic.h>
enum memory_order

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

} ;
(depuis C11)

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 multicœur, lorsque plusieurs threads lisent et écrivent simultanément 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 le langage et 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 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 <stdatomic.h>
Valeur Explication
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épendante de la valeur actuellement chargée ne peut être réordonnée avant ce chargement. Les écritures vers des variables dépendantes des données dans d'autres threads 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).

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 = atomic_load_explicit ( y, memory_order_relaxed ) ; // A
atomic_store_explicit ( x, r1, memory_order_relaxed ) ; // B
// Thread 2:
r2 = atomic_load_explicit ( x, memory_order_relaxed ) ; // C
atomic_store_explicit ( y, 42 , 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.

L'utilisation typique de l'ordonnancement mémoire relaxé est l'incrémentation de compteurs, tels que les compteurs de référence, car cela ne nécessite que l'atomicité, mais pas l'ordonnancement ou la synchronisation.

Ordonnancement Release-Consume

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_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 happened-before le stockage atomique du point de vue du thread A, deviennent des visible side-effects au sein de ces opérations dans le thread B dans lesquelles l'opération de chargement carries dependency , 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 n'est établie qu'entre les threads libérant 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 à l'exception du DEC Alpha, l'ordonnancement des dépendances 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, il est interdit au compilateur d'effectuer des chargements spéculatifs sur les objets impliqués dans la chaîne de dépendance).

Les cas d'utilisation typiques pour cet ordonnancement impliquent l'accès en lecture à des structures de données concurrentes rarement modifiées (tables de routage, configurations, politiques de sécurité, règles de pare-feu, etc.) et les situations de type producteur-consommateur avec publication par pointeur, c'est-à-dire lorsque le producteur publie un pointeur via lequel le consommateur peut accéder aux informations : 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 .

Notez qu'actuellement (02/2015) aucun compilateur de production connu ne suit les chaînes de dépendance : les opérations consume sont élevées au niveau des opérations acquire.

Séquence de libération

Si un certain objet atomique est stocké avec une sémantique de libération (store-release) et que plusieurs autres threads effectuent des opérations de lecture-modification-écriture sur cet objet atomique, une "séquence de libération" est formée : tous les threads qui effectuent les lectures-modifications-écritures sur le même objet atomique se synchronisent avec le premier thread et entre eux, même s'ils n'ont pas de sémantique memory_order_release . Cela rend possible les situations avec un producteur unique et plusieurs consommateurs sans imposer une synchronisation inutile entre les threads consommateurs individuels.

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 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 les mutexes ou les atomic spinlocks , sont un exemple de synchronisation release-acquire : lorsque le verrou est libéré par le thread A et acquis par le thread B, tout ce qui s'est produit dans la section critique (avant la libération) 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.

Ordonnancement séquentiellement cohérent

Les opérations atomiques étiquetées memory_order_seq_cst non seulement ordonnent la mémoire de la même manière que l'ordonnancement release/acquire (tout ce qui s'est passé-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 memory_order_seq_cst opération B qui charge à partir de la variable atomique M, observe l'une des possibilités suivantes :

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

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

  • la dernière memory_order_seq_cst modification de M qui apparaît avant X dans l'ordre total unique,
  • une modification non liée de M qui apparaît ultérieurement 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 existe deux memory_order_seq_cst atomic_thread_fence X et Y, et si A est sequenced-before X, Y est sequenced-before 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 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 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 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 qu'un ordre total pour les barrières elles-mêmes, et non pour les opérations atomiques dans le cas général ( sequenced-before n'est pas une relation inter-threads, contrairement à happens-before ).

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.

Relation avec volatile

Dans un fil d'exécution, les accès (lectures et écritures) via des lvalues volatiles ne peuvent pas être réordonnés au-delà des effets secondaires observables (y compris d'autres accès volatiles) qui sont séparés par un point de séquence dans le même fil, mais cet ordre n'est pas garanti d'être observé par un autre fil, car l'accès volatile n'établit pas de synchronisation inter-fils.

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'acquisition ( Microsoft Docs ), et ainsi les volatiles peuvent être utilisés pour la synchronisation inter-threads. Les sémantiques standards du volatile ne sont pas applicables à la programmation multithread, bien qu'elles soient suffisantes pour, par exemple, la communication avec un gestionnaire de 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.

Exemples

Références

  • Norme C23 (ISO/CEI 9899:2024) :
  • 7.17.1/4 memory_order (p: TBD)
  • 7.17.3 Ordre et cohérence (p: TBD)
  • Norme C17 (ISO/CEI 9899:2018) :
  • 7.17.1/4 memory_order (p : 200)
  • 7.17.3 Ordre et cohérence (p : 201-203)
  • Norme C11 (ISO/CEI 9899:2011) :
  • 7.17.1/4 memory_order (p: 273)
  • 7.17.3 Ordre et cohérence (p: 275-277)

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