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.