Je lis la documentation et plus spécifiquement
memory_order_acquire : une opération de chargement avec cet ordre de mémoire est effectuée l'opération d'acquisition sur l'emplacement mémoire affecté: pas de lecture ou les écritures dans le thread actuel peuvent être réorganisées avant ce chargement. Tout écrit dans d'autres threads qui libèrent la même variable atomique sont visible dans le fil de discussion actuel (voir la commande Release-Acquire ci-dessous).
memory_order_release : une opération de stockage avec cet ordre de mémoire exécute l'opération de libération: aucune lecture ou écriture dans le courant le fil peut être réorganisé après ce magasin. Toutes les écritures dans le courant thread sont visibles dans d'autres threads qui acquièrent le même atomique variable (voir l'ordre Release-Acquire ci-dessous) et les écritures qui portent un la dépendance dans la variable atomique devient visible dans d'autres threads qui consomment le même atomique (voir l'ordre Release-Consume ci-dessous)
Ces deux bits:
de memory_order_acquire
... aucune lecture ou écriture dans le thread actuel ne peut être réordonnée avant ce chargement ...
de memory_order_release
... aucune lecture ou écriture dans le fil en cours ne peut être réorganisée après ce magasin ...
Que signifient-ils exactement?
Il y a aussi cet exemple
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
Mais je ne peux pas vraiment comprendre où les deux bits I ' J'ai cité appliquer. Je comprends ce qui se passe mais je ne vois pas vraiment le bit de réorganisation car le code est petit.
3 Réponses :
Si vous avez utilisé std :: memory_order_relaxed pour le magasin, le compilateur pourrait utiliser la règle "as-if" pour déplacer data = 42; après le magasin, et le consommateur pourrait voir un pointeur non nul et des données indéterminées.
Si vous avez utilisé std :: memory_order_relaxed pour le chargement, le compilateur pourrait utiliser la règle "as-if" pour déplacer assert (data == 42); avant la boucle de chargement.
Les deux sont autorisés car la valeur de data n'est pas liée à la valeur de ptr
Si à la place ptr n'était pas atomique, vous auriez une course aux données et donc un comportement indéfini.
Je ne suis pas sûr de comprendre. Pour autant que je sache, le memory_order_relaxed impliquerait une réorganisation des instructions, mais cela préserverait atomicité et cohérence de l'ordre . Mon interprétation de l'exemple dans ma question est qu'aucune instruction avant / après le magasin / chargement ne serait réordonnée, ce que j'ai du mal à comprendre, c'est en quoi cela diffère de la cohérence de la commande garantie par le modèle détendu. Existe-t-il un meilleur exemple (code) qui peut le montrer?
@ user8469759 Si vous effectuez un tas de magasins memory_order_relaxed vers une seule variable sur un thread, les autres threads ne peuvent pas voir ceux dans le désordre (par exemple, l'incrémentation d'un compteur reste monotone), mais il n'y a aucune contrainte sur opérations affectant d'autres variables.
@ user8469759 Je pense que le assert (* p2 == "Hello"); dans l'exemple est un hareng rouge. La seule façon qui peut sembler se déclencher est dans le cas de comportement non atomique et indéfini.
@ user8469759 D'où vous vient le fait que l'ordre de mémoire assoupli fournit la cohérence de l'ordre ? AFAIK, les opérations détendues ne garantissent que l'atomicité et n'apportent aucune garantie de commande.
De la page que j'ai liée, je cite: * Les opérations atomiques marquées memory_order_relaxed ne sont pas des opérations de synchronisation; ils n'imposent pas d'ordre entre les accès mémoire simultanés. Ils garantissent uniquement l'atomicité et la cohérence des ordres de modification. * Sauf si j'ai mal compris
@ user8469759 c'est ce que vous ne comprenez pas. "cohérence de l'ordre de modification" se réfère uniquement aux modifications apportées à cette variable
Je n'obtiens toujours pas la différence avec la release et la acquisition , est-ce que la lecture / écriture mentionnée s'applique également à d'autres variables?
Vous voudrez peut-être jeter un œil à cette conférence: elle est longue, mais il donne vraiment de bonnes explications sur tous les détails de l'atomique et du modèle de mémoire: channel9.msdn.com/Shows/Going+Deep/...
@ user8469759 release n'a pas de sens pour les charges, et acquis n'a pas de sens pour les magasins. Ils sont distincts car un échange ne peut désirer ni l'un ni l'autre, ni les deux.
Puis-je vous demander quel type de modification dois-je apporter à l'extrait de code que j'ai publié afin de voir les effets de ces modes? Il y a un exemple dans la page que j'ai liée avec le mode consommer où assert (data == 42) devrait se déclencher, dans mon cas, rien ne se passe. Y a-t-il peut-être un exemple un peu plus élaboré que je peux exécuter?
Il n'y a pas de changement sensé qui signifie que l'assert devrait se déclencher. Mettre std :: memory_order_relaxed à la place signifie que l'assert peut se déclencher et que l'implémentation est toujours conforme au standard
Une implémentation conforme peut choisir d'ignorer ce que vous transmettez et de tout traiter de la même manière que std :: memory_order_seq_cst
Pour être honnête, j'ai besoin d'une réponse détaillée ... Je vais regarder la vidéo suggérée par @mvd et voir comment ça se passe.
Question idiote, toujours en train de regarder la vidéo, est-ce essentiellement cette acquisition / libération sémantique ce que fait verrouiller / déverrouiller sur un mutex? Donc, dans l'exemple, un thread verrouille / acquiert et l'autre déverrouille / libère , n'est-ce pas?
Quelqu'un est-il prêt à discuter avec moi pour clarifier cela?
Le travail effectué par un thread n'est pas garanti d'être visible pour les autres threads.
Pour rendre les données visibles entre les threads, un mécanisme de synchronisation est nécessaire. Un atomique non relaxé ou un mutex peut être utilisé pour cela. C'est ce qu'on appelle la sémantique d'acquisition-libération. L'écriture d'un mutex "libère" toutes les écritures en mémoire avant lui et la lecture du même mutex "acquiert" ces écritures.
Ici, nous utilisons ptr pour "libérer" le travail effectué jusqu'à présent ( data = 42 ) à un autre thread:
while (!ptr.load(std::memory_order_acquire)) // assuming initially ptr is null
;
assert(data == 42);
Et ici nous attendons cela, et en faisant cela nous synchroniser ("acquérir") le travail effectué par le fil producteur:
data = 42;
ptr.store(p, std::memory_order_release); // changes ptr from null to not-null
Notez deux actions distinctes:
En l'absence de (2), par ex. lors de l'utilisation de memory_order_relaxed , seule la valeur atomic elle-même est synchronisée. Tous les autres travaux effectués avant / après ne le sont pas, par ex. data ne contiendra pas nécessairement 42 et il se peut qu'il n'y ait pas d'instance de string entièrement construite à l'adresse p ( comme vu par le consommateur).
Pour plus de détails sur la sémantique d'acquisition / de publication et d'autres détails sur le modèle de mémoire C ++, je recommanderais de regarder l'excellent Herb discussion sur les armes atomiques sur channel9 , c'est long mais amusant à regarder. Et pour encore plus de détails, il existe un livre intitulé "C ++ Concurrency in Action" .
En référence à la documentation, que signifie «réorganiser avant / après»? (à savoir les deux citations spécifiques dans ma question).
ok, cela a beaucoup de sens. mais soulève la question que signifie ptr.store (p, std :: memory_order_aquire) ? Si cela n'a absolument aucun sens, alors pourquoi est-ce une option?
@UKMonkey dont les valeurs de commande peuvent être spécifiées pour un magasin sont spécifiées dans [atomics.order] . memory_order_acquire n'en fait pas partie, car cela n'a aucun sens. Donc, à proprement parler, le code ptr.store (p, std :: memory_order_acquire) invoquerait un comportement indéfini.
@rustyx merci :) Je pense que je comprends tout un peu mieux maintenant!
L'acquisition et la libération sont des barrières de mémoire.
Si votre programme lit des données après une barrière d'acquisition, vous êtes assuré que vous lirez des données cohérentes avec toute version précédente par tout autre thread en ce qui concerne la même variable atomique. Les variables atomiques sont garanties d'avoir un ordre absolu (lors de l'utilisation de memory_order_acquire et memory_order_release bien que des opérations plus faibles soient prévues) pour leurs lectures et écritures sur tous les threads. Ces barrières propagent en effet cet ordre à tous les threads utilisant cette variable atomique.
Vous pouvez utiliser atomics pour indiquer que quelque chose est `` terminé '' ou est `` prêt '', mais si le consommateur lit au-delà de cette variable atomique, le consommateur ne peut pas compter sur `` voir '' les bonnes `` versions '' d'autres mémoires et atomiques aurait une valeur limitée .
Les instructions concernant "déplacer avant" ou "déplacer après" sont des instructions à l'optimiseur de ne pas réorganiser les opérations pour qu'elles se déroulent dans le désordre. Les optimiseurs sont très bons pour réorganiser les instructions et même omettre les lectures / écritures redondantes, mais s'ils réorganisent le code à travers les barrières de mémoire, ils peuvent involontairement violer cet ordre.
Votre code repose sur le fait que l'objet std :: string (a) a été construit dans producteur () avant que ptr ne soit attribué et ( b) la version construite de cette chaîne (c'est-à-dire la version de la mémoire qu'elle occupe) étant celle que consumer () lit.
En termes simples, consumer () va lire avec impatience la chaîne dès qu'il voit ptr assigné, donc il vaut mieux voir un objet valide et entièrement construit, sinon de mauvais temps s'ensuivront .
Dans ce code, «l'acte» d'attribuer ptr est la façon dont producteur () "dit au" consommateur que la chaîne est "prête". La barrière de la mémoire existe pour s'assurer que c'est ce que voit le consommateur.
Inversement, si ptr était déclaré comme un std :: string * ordinaire, alors le compilateur pourrait décider d'optimiser p loin et attribuer les adresse directement à ptr et seulement ensuite construire l'objet et attribuer les données int . C'est probablement un désastre pour le thread consommateur qui utilise cette affectation comme indicateur que les objets que producteur prépare sont prêts.
Pour être précis si ptr était un pointeur, le consommateur peut ne jamais voir la valeur assignée ou sur certaines architectures lire une valeur partiellement assignée où seuls certains des octets ont été assignés et il pointe vers un emplacement mémoire de garbage. Cependant, ces aspects concernent le fait qu'il est atomique et non les barrières de mémoire plus larges.
Je pense que votre réponse est la plus complète. Pouvez-vous également examiner cette question? stackoverflow.com/questions/59651328/… < / a>
"Les variables atomiques sont garanties d'avoir un ordre absolu pour leurs lectures et écritures sur tous les threads." Est-ce vraiment vrai? Je pense que cette garantie est fournie par SC ou plus fort, pas par l'atomique en général.
@eric Fair commentaire. memory_order_acquire et memory_order_release 'assurent la cohérence séquentielle des atomiques mais des opérations plus faibles telles que memory_order_relaxed` sont prévues. J'ai modifié la réponse en conséquence. C'est un domaine très subtil et j'apprécie toutes les corrections qui améliorent la clarté.
@Persixty, comment memory_order_acquire et memory_order_release assurent la cohérence séquentielle?
@Eric Parce qu'ils impliquent des barrières de mémoire, donc les autres threads qui accèdent à la même variable atomique connaîtront une cohérence séquentielle dans le sens où dans l'exemple ci-dessus l'objet std :: string semblera s'être produit (en entier) avant que le pointeur atomique ne soit défini.
@Persixty, d'accord, je vois ce que vous essayez de dire mais il est faux de décrire cela comme une cohérence séquentielle. La cohérence séquentielle garantit que tous les accès mémoire ont un ordre total. Ce n'est pas vrai pour la sémantique d'acquisition / de publication car les threads peuvent observer des mises à jour dans des ordres différents.
@Persixty, je pense que ce que vous essayez de décrire est la relation de synchronisation avec. Autrement dit, la libération en écriture se synchronise avec la lecture-acquisition et toutes les écritures qui se produisent avant la libération en écriture deviennent des effets secondaires visibles dans le thread exécutant la lecture-acquisition.
@Eric Je suis mais avec la terminologie minimale déroutante importée.