Qu'est-ce qui est garanti avec C ++ std :: atomic au niveau du programmeur?

9

J'ai écouté et lu plusieurs articles, discussions et questions sur le stackoverflow std::atomic, et je voudrais être sûr d'avoir bien compris. Parce que je suis toujours un peu confus avec la visibilité des écritures de la ligne de cache en raison de retards possibles dans les protocoles de cohérence de cache MESI (ou dérivés), les tampons de stockage, les files d'attente invalides, etc.

J'ai lu que x86 a un modèle de mémoire plus fort, et que si une invalidation de cache est retardée, x86 peut annuler les opérations démarrées. Mais je ne m'intéresse maintenant qu'à ce que je devrais assumer en tant que programmeur C ++, indépendamment de la plateforme.

[T1: thread1 T2: thread2 V1: variable atomique partagée]

Je comprends que std :: atomic garantit que,

(1) Aucune course de données ne se produit sur une variable (grâce à l'accès exclusif à la ligne de cache).

(2) En fonction de l'ordre mémoire que nous utilisons, il garantit (avec des barrières) que la cohérence séquentielle se produit (avant une barrière, après une barrière ou les deux).

(3) Après une écriture atomique (V1) sur T1, une RMW atomique (V1) sur T2 sera cohérente (sa ligne de cache aura été mise à jour avec la valeur écrite sur T1).

Mais comme le mentionne l' amorce de cohérence du cache ,

L'implication de toutes ces choses est que, par défaut, les charges peuvent récupérer des données périmées (si une demande d'invalidation correspondante se trouvait dans la file d'attente d'invalidation)

Alors, est-ce que ce qui suit est correct?

(4) std::atomicne garantit PAS que T2 ne lira pas une valeur «périmée» sur une lecture atomique (V) après une écriture atomique (V) sur T1.

Questions si (4) a raison: si l'écriture atomique sur T1 invalide la ligne de cache quel que soit le retard, pourquoi T2 attend-il que l'invalidation soit effective quand une opération RMW atomique mais pas sur une lecture atomique?

Questions si (4) est faux: quand un thread peut-il lire une valeur "périmée" et "c'est visible" dans l'exécution, alors?

J'apprécie beaucoup vos réponses

Mise à jour 1

Il semble donc que j'avais tort sur (3). Imaginez l'entrelacement suivant, pour un V1 initial = 0:

T1: W(1)
T2:      R(0) M(++) W(1)

Même si RMW de T2 est garanti de se produire entièrement après W (1) dans ce cas, il peut toujours lire une valeur «périmée» (j'avais tort). Selon cela, atomic ne garantit pas la cohérence complète du cache, seulement la cohérence séquentielle.

Mise à jour 2

(5) Imaginez maintenant cet exemple (x = y = 0 et sont atomiques):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

selon ce dont nous avons parlé, voir le "msg" affiché à l'écran ne nous donnerait pas d'informations au-delà du fait que T2 a été exécuté après T1. Il se peut donc que l'une des exécutions suivantes se soit produite:

  • T1 <T3 <T2
  • T1 <T2 <T3 (où T3 voit x = 1 mais pas encore y = 1)

Est-ce correct?

(6) Si un thread peut toujours lire des valeurs «périmées», que se passerait-il si nous prenions le scénario typique de «publication» mais au lieu de signaler que certaines données sont prêtes, nous faisons exactement le contraire (supprimer les données)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

où T2 utiliserait toujours un ptr supprimé jusqu'à ce que is_enabled soit faux.

(7) De plus, le fait que les threads puissent lire des valeurs «périmées» signifie qu'un mutex ne peut pas être implémenté avec un seul droit atomique sans verrouillage? Cela nécessiterait un mécanisme de synchronisation entre les threads. Aurait-il besoin d'un atomique verrouillable?

Albert Caldas
la source

Réponses:

3
  1. Oui, il n'y a pas de course aux données
  2. Oui, avec des memory_ordervaleurs appropriées , vous pouvez garantir une cohérence séquentielle
  3. Une lecture-modification-écriture atomique se produira toujours entièrement avant ou entièrement après une écriture atomique dans la même variable
  4. Oui, T2 peut lire une valeur périmée d'une variable après une écriture atomique sur T1

Les opérations atomiques de lecture-modification-écriture sont spécifiées de manière à garantir leur atomicité. Si un autre thread pouvait écrire dans la valeur après la lecture initiale et avant l'écriture d'une opération RMW, cette opération ne serait pas atomique.

Les threads peuvent toujours lire les valeurs périmées, sauf lorsque se produit avant garantit un ordre relatif .

Si une opération RMW lit une valeur "périmée", elle garantit que l'écriture qu'elle génère sera visible avant toute écriture à partir d'autres threads qui écraserait la valeur lue.

Mettre à jour par exemple

Si T1 écrit x=1et T2 fait x++, avec xinitialement 0, les choix du point de vue du stockage de xsont:

  1. L'écriture de T1 est d'abord, donc T1 écrit x=1, puis T2 lit x==1, incrémente cela à 2 et réécrit x=2comme une seule opération atomique.

  2. L'écriture de T1 est la deuxième. T2 lit x==0, l'incrémente à 1, et réécrit x=1en une seule opération, puis T1 écrit x=1.

Cependant, à condition qu'il n'y ait pas d'autres points de synchronisation entre ces deux threads, les threads peuvent poursuivre les opérations non vidées en mémoire.

Ainsi, T1 peut émettre x=1, puis procéder à d'autres choses, même si T2 continuera de lire x==0(et donc d'écrire x=1).

S'il existe d'autres points de synchronisation, il apparaîtra alors quel thread a été modifié en xpremier, car ces points de synchronisation forceront une commande.

Cela est plus apparent si vous avez une condition sur la valeur lue à partir d'une opération RMW.

Mise à jour 2

  1. Si vous utilisez memory_order_seq_cst(la valeur par défaut) pour toutes les opérations atomiques, vous n'avez pas à vous soucier de ce genre de chose. Du point de vue du programme, si vous voyez "msg" alors T1 a couru, puis T3, puis T2.

Si vous utilisez d'autres ordres de mémoire (en particulier memory_order_relaxed), vous pouvez voir d'autres scénarios dans votre code.

  1. Dans ce cas, vous avez un bug. Supposons que le is_enableddrapeau soit vrai, lorsque T2 entre dans sa whileboucle, il décide donc d'exécuter le corps. T1 supprime maintenant les données, et T2 diffère ensuite le pointeur, qui est un pointeur suspendu, et un comportement indéfini s'ensuit. L'atomique n'aide ni n'entrave en rien au-delà d'empêcher la course aux données sur le drapeau.

  2. Vous pouvez implémenter un mutex avec une seule variable atomique.

Anthony Williams
la source
Merci beaucoup @Anthony Wiliams pour votre réponse rapide. J'ai mis à jour ma question avec un exemple de RMW lisant une valeur «périmée». En regardant cet exemple, que voulez-vous dire par ordre relatif et que le W (1) de T2 sera visible avant toute écriture? Cela signifie-t-il qu'une fois que T2 aura vu les changements de T1, il ne lira plus W (1) de T2?
Albert Caldas
Donc, si «les threads peuvent toujours lire les valeurs périmées», cela signifie que la cohérence du cache n'est jamais garantie (au moins au niveau du programmeur c ++). Pourriez-vous jeter un œil à ma mise à jour2 s'il vous plaît?
Albert Caldas
Maintenant, je vois que j'aurais dû prêter plus d'attention aux modèles de mémoire de langage et de matériel pour bien comprendre tout cela, c'était la pièce qui me manquait. Merci beaucoup!
Albert Caldas
1

Concernant (3) - cela dépend de l'ordre de la mémoire utilisée. Si les deux, le magasin et l'opération RMW utilisent std::memory_order_seq_cst, alors les deux opérations sont ordonnées d'une manière ou d'une autre - c'est-à-dire que le magasin se produit avant le RMW, ou l'inverse. Si le magasin est commandé avant le RMW, alors il est garanti que l'opération RMW "voit" la valeur qui a été stockée. Si le magasin est commandé après le RMW, il écraserait la valeur écrite par l'opération RMW.

Si vous utilisez des ordres de mémoire plus détendus, les modifications seront toujours ordonnées d'une certaine manière (l'ordre de modification de la variable), mais vous n'avez aucune garantie si le RMW "voit" la valeur de l'opération de stockage - même si l'opération RMW est l'ordre après l'écriture dans l'ordre de modification de la variable.

Si vous souhaitez lire un autre article, je peux vous référer aux modèles de mémoire pour les programmeurs C / C ++ .

mpoeter
la source
Merci pour l'article, je ne l'avais pas encore lu. Même si c'est assez vieux, ça a été utile de rassembler mes idées.
Albert Caldas
1
Heureux d'entendre cela - cet article est un chapitre légèrement étendu et révisé de la thèse de mon maître. :-) Il se concentre sur le modèle de mémoire tel qu'introduit C ++ 11; Je pourrais le mettre à jour pour refléter les (petits) changements introduits dans C ++ 14/17. N'hésitez pas à me faire part de vos commentaires ou suggestions d'amélioration!
mpoeter