web-dev-qa-db-fra.com

Que signifie chaque memory_order?

J'ai lu un chapitre et je n'ai pas beaucoup aimé. Je ne sais toujours pas quelles sont les différences entre chaque ordre de mémoire. C'est ma spéculation actuelle que j'ai compris après avoir lu le plus simple http://en.cppreference.com/w/cpp/atomic/memory_order

Ce qui suit est faux alors n'essayez pas d'en tirer des leçons

  • memory_order_relaxed: ne se synchronise pas mais n'est pas ignoré lorsque l'ordre est effectué à partir d'un autre mode dans un var atomique différent
  • memory_order_consume: Synchronise la lecture de cette variable atomique mais ne synchronise pas les variables détendues écrites avant cela. Cependant, si le thread utilise var X lors de la modification de Y (et le libère). D'autres threads consommant Y verront-ils également X sortir? Je ne sais pas si cela signifie que ce thread repousse les changements de x (et évidemment y)
  • memory_order_acquire: synchronise la lecture de cette variable atomique ET s'assure que les variables détendues écrites avant celle-ci sont également synchronisées. (cela signifie-t-il que toutes les variables atomiques sur tous les threads sont synchronisées?)
  • memory_order_release: pousse le magasin atomique vers d'autres threads (mais seulement s'ils lisent la var avec consume/acquisition)
  • memory_order_acq_rel: pour les opérations de lecture/écriture. Effectue une acquisition afin de ne pas modifier une ancienne valeur et libère les modifications.
  • memory_order_seq_cst: la même chose que la version d'acquisition sauf qu'elle force les mises à jour à être vues dans d'autres threads (si a stocke avec relax sur un autre thread. Je stocke b avec seq_cst. Un 3ème thread lit a avec relax verra des changements avec b et toute autre variable atomique?).

Je pense avoir compris mais corrigez-moi si je me trompe. Je n'ai rien trouvé qui l'explique en anglais facile à lire.

51
user34537

Le GCC Wiki donne un explication très approfondie et facile à comprendre avec des exemples de code.

(extrait édité et emphase ajoutée)

IMPORTANT:

En relisant la citation ci-dessous copiée à partir du Wiki GCC dans le processus d'ajout de ma propre formulation à la réponse, j'ai remarqué que la citation est en fait incorrecte. Ils ont obtenu acquérir et consommer exactement dans le mauvais sens. Une opération release-consume fournit uniquement une garantie de commande sur les données dépendantes tandis qu'une opération release-acquisition fournit cette garantie indépendamment du fait que les données dépendent de la valeur atomique ou ne pas.

Le premier modèle est "séquentiellement cohérent". C'est le mode par défaut utilisé quand aucun n'est spécifié, et c'est le plus restrictif. Il peut également être spécifié explicitement via memory_order_seq_cst. Il fournit les mêmes restrictions et limitations au déplacement des charges que les programmeurs séquentiels sont intrinsèquement familières, sauf qu'il s'applique à travers les threads .
[...]
D'un point de vue pratique, cela revient à toutes les opérations atomiques agissant comme des barrières d'optimisation. C'est OK de réorganiser les choses entre les opérations atomiques, mais pas à travers l'opération. Les éléments locaux des threads ne sont pas non plus affectés car il n'y a pas de visibilité sur les autres threads. [...] Ce mode offre également une cohérence sur tous les threads.

L'approche opposée est memory_order_relaxed. Ce modèle permet une synchronisation beaucoup moins importante en supprimant les restrictions qui se produisent avant. Ces types d'opérations atomiques peuvent également faire l'objet de diverses optimisations, telles que la suppression et la mise en commun des magasins morts. [...] Sans aucun bord avant, aucun thread ne peut compter sur un ordre spécifique d'un autre thread.
Le mode détendu est le plus couramment utilisé lorsque le programmeur veut simplement qu'une variable soit de nature atomique plutôt que de l'utiliser pour synchroniser les threads pour d'autres données de mémoire partagée.

Le troisième mode (memory_order_acquire/memory_order_release) est un hybride entre les deux autres. Le mode d'acquisition/libération est similaire au mode séquentiel cohérent, sauf qu'il applique uniquement une relation passe-avant aux variables dépendantes . Cela permet un relâchement de la synchronisation requise entre les lectures indépendantes des écritures indépendantes.

memory_order_consume est un autre raffinement subtil dans le modèle de mémoire de libération/acquisition qui assouplit légèrement les exigences en supprimant ce qui se produit avant de commander sur des variables partagées non dépendantes aussi.
[...]
La vraie différence se résume à la quantité d’état que le matériel doit vider pour se synchroniser. Étant donné qu'une opération de consommation peut-être s'exécute donc plus rapidement, une personne qui sait ce qu'elle fait peut l'utiliser pour des applications critiques en termes de performances.

Voici ma propre tentative d'explication plus banale:

Une approche différente pour le regarder est de considérer le problème du point de vue de la réorganisation des lectures et des écritures, atomiques et ordinaires:

Toutes les opérations atomiques sont garanties atomiques en elles-mêmes (la combinaison de deux les opérations atomiques ne sont pas atomiques dans leur ensemble !) et être visible dans l'ordre total dans lequel ils apparaissent sur la timeline du flux d'exécution. Cela signifie qu'aucune opération atomique ne peut, en aucun cas, être réorganisée, mais d'autres opérations de mémoire pourraient très bien l'être. Les compilateurs (et les processeurs) effectuent régulièrement ce réarrangement comme une optimisation.
Cela signifie également que le compilateur doit utiliser toutes les instructions nécessaires pour garantir qu'une opération atomique exécutée à tout moment verra les résultats de chaque opération atomique, éventuellement sur un autre cœur de processeur (mais pas nécessairement d'autres opérations) , qui ont été exécutés auparavant.

Maintenant, un détendu n'est que cela, le strict minimum. Il ne fait rien de plus et n'offre aucune autre garantie. C'est l'opération la moins chère possible. Pour les opérations de non-lecture-modification-écriture sur des architectures de processeur fortement ordonnées (par exemple x86/AMD64), cela se résume à un mouvement ordinaire normal.

L'opération séquentielle cohérente est l'exact opposé, elle impose un ordre strict non seulement pour les opérations atomiques, mais aussi pour les autres opérations de mémoire qui se produisent avant ou après. Aucun des deux ne peut franchir la barrière imposée par l'opération atomique. En pratique, cela signifie des opportunités d'optimisation perdues, et il se peut que des instructions de clôture doivent être insérées. C'est le modèle le plus cher.

Une opération de relâchement empêche la réorganisation des charges et des stockages ordinaires après l'opération atomique, tandis qu'une opération l'opération d'acquisition empêche la réorganisation des charges et des magasins ordinaires avant l'opération atomique. Tout le reste peut encore être déplacé.
La combinaison d'empêcher les magasins d'être déplacés après et de déplacer les charges avant l'opération atomique respective garantit que tout ce que le thread acquéreur voit est cohérent, avec seulement une petite quantité d'opportunité d'optimisation perdue.
On peut penser à cela comme quelque chose comme un verrou inexistant qui est libéré (par l'écrivain) et acquis (par le lecteur). Sauf ... il n'y a pas de serrure.

En pratique, la libération/l'acquisition signifie généralement que le compilateur n'a pas besoin d'utiliser d'instructions spéciales particulièrement coûteuses, mais il ne peut pas réorganiser librement les charges et les magasins à son goût, ce qui peut manquer certaines (petites) opportunités d'optimisation .

Enfin, consommer est la même opération que acquérir, à la seule exception près que les garanties de commande ne s'appliquent qu'aux données dépendantes . Les données dépendantes seraient par exemple être des données pointées par un pointeur modifié atomiquement.
On peut dire que cela peut fournir quelques opportunités d'optimisation qui ne sont pas présentes avec les opérations d'acquisition (car moins de données sont soumises à des restrictions), mais cela se produit au détriment d'un code plus complexe et plus sujet aux erreurs, et la tâche non triviale d'obtenir des chaînes de dépendance correctes.

Il est actuellement déconseillé d'utiliser la commande consommer pendant la révision de la spécification.

48
Damon

C'est un sujet assez complexe. Essayez de lire http://en.cppreference.com/w/cpp/atomic/memory_order plusieurs fois, essayez de lire d'autres ressources, etc.

Voici une description simplifiée:

Le compilateur et CPU peut réorganiser les accès mémoire. Autrement dit, ils peuvent se produire dans un ordre différent de celui spécifié dans le code. C'est très bien la plupart du temps, le problème se pose lorsque différents threads tentent de communiquer et peuvent voir un tel ordre d'accès à la mémoire qui brise les invariants du code.

Habituellement, vous pouvez utiliser des verrous pour la synchronisation. Le problème est qu'ils sont lents. Les opérations atomiques sont beaucoup plus rapides, car la synchronisation se produit au niveau du processeur (c'est-à-dire que le processeur garantit qu'aucun autre thread, même sur un autre processeur, ne modifie une variable, etc.).

Ainsi, le seul problème auquel nous sommes confrontés est la réorganisation des accès à la mémoire. Le memory_order enum spécifie quels types de compilateur de réordonnances doit interdire.

relaxed - pas de contraintes.

consume - aucune charge qui dépend de la nouvelle valeur chargée ne peut être réorganisée par rapport à. la charge atomique. C'est à dire. s'ils sont après la charge atomique dans le code source, ils se produiront après la charge atomique aussi.

acquire - aucune charge ne peut être réorganisée par écrit. la charge atomique. C'est à dire. s'ils sont après la charge atomique dans le code source, ils se produiront après la charge atomique aussi.

release - aucun magasin ne peut être réorganisé par écrit. le magasin atomique. C'est à dire. s'ils sont avant le magasin atomique dans le code source, ils se produiront avant le magasin atomique aussi.

acq_rel - acquire et release combinés.

seq_cst - il est plus difficile de comprendre pourquoi cette commande est requise. Fondamentalement, tous les autres classements garantissent uniquement que les réorganisations spécifiques non autorisées ne se produisent pas uniquement pour les threads qui consomment/libèrent la même variable atomique. Les accès en mémoire peuvent toujours se propager à d'autres threads dans n'importe quel ordre. Cet ordre garantit que cela ne se produit pas (donc la cohérence séquentielle). Pour un cas où cela est nécessaire, voir l'exemple à la fin de la page liée.

26
user283145