118 votes

Que signifie chaque memory_order ?

J'ai lu un chapitre et je ne l'ai pas beaucoup aimé. Je ne suis toujours pas clair sur ce que sont les différences entre chaque ordre de mémoire. Ceci est ma spéculation actuelle que j'ai comprise après avoir lu le lien beaucoup plus simple http://en.cppreference.com/w/cpp/atomic/memory_order

Le ci-dessous est faux donc ne pas essayer d'apprendre de cela

  • memory_order_relaxed: Ne synchronise pas mais n'est pas ignoré lorsque l'ordre est effectué à partir d'un autre mode dans une variable atomique différente
  • memory_order_consume: Synchronise la lecture de cette variable atomique cependant cela ne synchronise pas les variables relaxées écrites avant cela. Cependant, si le thread utilise la variable X lors de la modification de Y (et la libère). Les autres threads qui consomment Y verront aussi X libéré? Je ne sais pas si cela signifie que ce thread pousse les changements de x (et évidemment de y)
  • memory_order_acquire: Synchronise la lecture de cette variable atomique ET s'assure que les variables relaxées écrites avant cela sont également synchronisées. (est-ce que cela signifie que toutes les variables atomiques sur tous les threads sont synchronisées?)
  • memory_order_release: Pousse le stockage atomique vers d'autres threads (mais seulement s'ils lisent la variable avec consume/acquire)
  • memory_order_acq_rel: Pour les opérations de lecture/écriture. Effectue un acquire afin que vous ne modifiez pas une ancienne valeur et libère les changements.
  • memory_order_seq_cst: La même chose qu'acquire release sauf que cela force les mises à jour à être vues dans les autres threads (si a est stocké avec relax sur un autre thread. Je stocke b avec seq_cst. Un 3ème thread lisant a avec relax verra les changements ainsi que b et toute autre variable atomique?).

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

123voto

Damon Points 26437

Le Wiki GCC donne une 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 du Wiki GCC dans le processus d'ajout de mes propres mots à la réponse, j'ai remarqué que la citation est en fait fausse. Ils ont inversé acquire et consume exactement de la mauvaise manière. Une opération de release-consume ne fournit qu'une garantie de l'ordre sur les données dépendantes, alors qu'une opération de release-acquire fournit cette garantie indépendamment de la dépendance des données sur la valeur atomique ou non.

Le premier modèle est "séquentiellement consistant". C'est le mode par défaut utilisé lorsque rien 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 connaissent intrinsèquement, sauf qu'il s'applique à travers des threads.
[...]
D'un point de vue pratique, cela revient à considérer toutes les opérations atomiques comme des barrières d'optimisation. Il est possible de réorganiser les choses entre les opérations atomiques, mais pas à travers l'opération. Les éléments locaux au thread ne sont pas affectés non plus car il n'y a aucune visibilité aux autres threads. [...] Ce mode assure également la cohérence à travers tous les threads.

L'approche opposée est memory_order_relaxed. Ce modèle permet une synchronisation beaucoup moins stricte en supprimant les restrictions de happens-before. Ces types d'opérations atomiques peuvent également faire l'objet de diverses optimisations, telles que la suppression des instructions mortes et la mise en commun. [...] Sans aucune relation happens-before, aucun thread ne peut compter sur un ordre spécifique en provenance d'un autre thread.
Le mode relaxé est le plus couramment utilisé lorsque le programmeur souhaite simplement qu'une variable soit atomique en nature plutôt que de l'utiliser pour synchroniser des threads à d'autres données en mémoire partagée.

Le troisième mode (memory_order_acquire / memory_order_release) est un hybride entre les deux autres. Le mode acquire/release est similaire au mode séquentiellement consistant, sauf qu'il n'applique une relation happens-before qu'aux variables dépendantes. Cela permet une relaxation de la synchronisation requise entre les lectures indépendantes des écritures indépendantes.

memory_order_consume est un raffinement subtil supplémentaire dans le modèle de mémoire release/acquire qui assouplit légèrement les exigences en supprimant l'ordre happens before sur les variables partagées non dépendantes également.
[...]
La vraie différence réside dans la quantité d'état que le matériel doit vider pour se synchroniser. Étant donné qu'une opération de consommation peut donc s'exécuter plus rapidement, quelqu'un qui sait ce qu'il fait peut l'utiliser pour des applications critiques en termes de performances.

Voici ma propre tentative d'explication plus banale :

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

Toutes les opérations atomiques sont garanties d'être atomiques en elles-mêmes (la combinaison de deux opérations atomiques n'est pas atomique dans son ensemble !) et d'être visibles dans l'ordre total dans lequel elles apparaissent sur la chronologie du flux d'exécution. Cela signifie qu'aucune opération atomique ne peut, dans aucune circonstance, être réorganisée, mais d'autres opérations sur la mémoire pourraient très bien l'être. Les compilateurs (et les processeurs) font régulièrement de telles réorganisations pour optimiser le processus.
Cela signifie également que le compilateur doit utiliser toutes les instructions nécessaires pour garantir qu'une opération atomique exécutée à un moment donné verra les résultats de chaque autre opération atomique précédemment exécutée, éventuellement sur un autre cœur de processeur (mais pas nécessairement pour d'autres opérations).

Maintenant, une opération relaxée est simplement cela, le strict minimum. Elle ne fait rien en plus et n'offre pas d'autres garanties. C'est l'opération la moins chère. Pour les opérations non en lecture-modification-écriture sur des architectures de processeur fortement ordonnées (par exemple x86/amd64), cela se résume à un simple mouvement normal et ordinaire.

L'opération séquentiellement consistante est l'exact opposé, elle impose un ordre strict non seulement pour les opérations atomiques, mais aussi pour les autres opérations sur la mémoire qui se produisent avant ou après. Aucune ne peut franchir la barrière imposée par l'opération atomique. En pratique, cela signifie des opportunités d'optimisation perdues, et éventuellement des instructions de barrière pourraient devoir être insérées. C'est le modèle le plus coûteux.

Une opération release empêche les charges et les écritures ordinaires d'être réorganisées après l'opération atomique, tandis qu'une opération acquire empêche les charges et les écritures ordinaires d'être réorganisées avant l'opération atomique. Tout le reste peut toujours être déplacé.
La combinaison d'empêcher les écritures d'être déplacées après, et les lectures d'être déplacées avant l'opération atomique respective garantit que ce que le thread acquérant voit est cohérent, avec seulement une petite quantité d'opportunités d'optimisation perdues.
On peut considérer cela comme quelque chose comme un verrou inexistant qui est libéré (par l'écrivain) et acquis (par le lecteur). Sauf que... il n'y a pas de verrou.

En pratique, release/acquire signifie généralement que le compilateur ne doit pas utiliser d'instructions spéciales particulièrement coûteuses, mais il ne peut pas réorganiser librement les charges et les écritures à sa guise, ce qui peut faire perdre certaines (petites) opportunités d'optimisation.

Enfin, consume est la même opération qu'acquire, sauf qu'elle garantit l'ordre seulement pour les données dépendantes. Les données dépendantes pourraient par exemple être des données pointées par un pointeur modifié de façon atomique.
On pourrait soutenir que cela peut offrir quelques opportunités d'optimisation absentes avec les opérations acquire (car moins de données sont soumises à des restrictions), cependant cela se fait au prix de code plus complexe et plus sujet aux erreurs, ainsi que la tâche non triviale de bien établir les chaînes de dépendance.

Il est actuellement déconseillé d'utiliser l'ordonnancement consume tandis que la spécification est en cours de révision.

45voto

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 le processeur peuvent 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 généralement bien, mais le problème survient lorsque différents threads tentent de communiquer et peuvent voir un tel ordre d'accès mémoire qui casse les invariants du code.

En général, 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 fait au niveau du processeur (c'est-à-dire que le processeur s'assure qu'aucun autre thread, même sur un autre processeur, ne modifie une variable, etc.).

Donc, le seul problème auquel nous sommes confrontés est la réorganisation des accès mémoire. L'énumération memory_order spécifie quels types de réorganisations le compilateur doit interdire.

relaxed - aucune contrainte.

consume - aucune charge dépendante de la valeur nouvellement chargée ne peut être réorganisée par rapport à la charge atomique. Autrement dit, si elles sont après la charge atomique dans le code source, elles se produiront également après la charge atomique.

acquire - aucune charge ne peut être réorganisée par rapport à la charge atomique. Autrement dit, si elles sont après la charge atomique dans le code source, elles se produiront également après la charge atomique.

release - aucune écriture ne peut être réorganisée par rapport à la sauvegarde atomique. Autrement dit, si elles sont avant la sauvegarde atomique dans le code source, elles se produiront également avant la sauvegarde atomique.

acq_rel - acquire et release combinés.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X