C ++ std :: memory_order_relaxed confusion

Oct 12 2020

Je lisais cet article du Wiki GCC sur les barrières de mémoire C ++ (et c'est génial).
C'était assez simple jusqu'à ce que j'arrive à ce point:

L'approche opposée est std :: memory_order_relaxed. Ce modèle permet beaucoup moins de synchronisation 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 de la mémoire morte et la mise en commun. Donc, dans l'exemple précédent:

-Thread 1-
y.store (20, memory_order_relaxed)
x.store (10, memory_order_relaxed)

-Thread 2-
if (x.load (memory_order_relaxed) == 10)
  {
    assert (y.load(memory_order_relaxed) == 20) /* assert A */
    y.store (10, memory_order_relaxed)
  }

-Thread 3-
if (y.load (memory_order_relaxed) == 10)
  assert (x.load(memory_order_relaxed) == 10) /* assert B */

Étant donné que les threads n'ont pas besoin d'être synchronisés à travers le système, l'une ou l'autre des assertions de cet exemple peut échouer.

Ok, c'est simple aussi, continuons…

-Thread 1-
x.store (1, memory_order_relaxed)
x.store (2, memory_order_relaxed)

-Thread 2-
y = x.load (memory_order_relaxed)
z = x.load (memory_order_relaxed)
assert (y <= z)

L'assertion ne peut échouer. Une fois que le stockage de 2 est vu par le thread 2, il ne peut plus voir la valeur 1. Cela empêche la fusion de charges relâchées d'une variable sur des charges relâchées d'une référence différente qui pourrait créer un alias.

C'est ce qui m'a dérouté, pourquoi y ne peut pas charger la valeur 2 et z charger la valeur 1 (et provoquer l'échec de l'assertion), puisque l'ordre n'est pas synchronisé dans le thread 1?

Réponses

4 NicolBolas Oct 12 2020 at 12:12

L'ordre détendu est relatif à l'ordre des opérations par rapport aux autres accès à la mémoire , et non à l'ordre par rapport à l'atome en cours de relâchement modifié. Dans votre premier cas, le fait que vous puissiez voir 10 dans xne veut rien dire par rapport à la valeur de y. Et vice versa.

Mais votre deuxième cas est différent, car il affecte le même objet atomique.

[intro.races] / 10 nous dit que, dans un thread, si une opération est séquencée avant une autre, alors cette opération "se produit avant" l'autre. Et [intro.races] / 14-17 décrivent le comportement suivant en ce qui concerne l'atomique :

Les quatre exigences de cohérence précédentes interdisent effectivement la réorganisation par le compilateur des opérations atomiques en un seul objet, même si les deux opérations sont des charges relâchées.

Et c'est ce que vous avez ici. Toutes les modifications se produisent sur le même objet, elles doivent donc se produire dans un certain ordre. Même si cet ordre ne peut pas être déterminé exactement, l'ordre doit respecter les relations «arrive avant» du code.

Les deux opérations du thread 1 sont ordonnées par une relation «arrive avant». Et les opérations de Thread 2 sont elles-mêmes ordonnées par une relation «arrive avant».

Puisqu'ils agissent tous sur le même objet atomique, si yobtient la valeur de 2, alors il doit avoir "arrivé après" a xété mis à 2. Donc l'ordre d'accès pour xdoit avoir été "x = 1, x = 2, lire x ". Et comme la dernière lecture de xse produit après la première lecture de x, la valeur obtenue ne peut pas être 1.