L'expression «survient avant» est utilisée plusieurs fois dans le projet de norme C ++.
Par exemple: Résiliation [basic.start.term] / 5
Si l'achèvement de l'initialisation d'un objet avec une durée de stockage statique se produit fortement avant un appel à std :: atexit (voir, [support.start.term]), l'appel à la fonction est passé à std :: atexit est séquencé avant l'appel au destructeur de l'objet. Si un appel à std :: atexit se produit fortement avant la fin de l'initialisation d'un objet avec une durée de stockage statique, l'appel au destructeur de l'objet est séquencé avant l'appel à la fonction passé à std :: atexit . Si un appel à std :: atexit se produit fortement avant un autre appel à std :: atexit, l'appel à la fonction passé au deuxième appel std :: atexit est séquencé avant l'appel à la fonction passé au premier appel std :: atexit.
Et défini dans les courses de données [intro.races] / 12
Une évaluation A se produit fortement avant une évaluation D si, soit
(12.1) A est séquencé avant D, ou
(12.2) A se synchronise avec D, et A et D sont des opérations atomiques séquentiellement cohérentes ([atomics.order]), ou
(12.3) il existe des évaluations B et C telles que A est séquencé avant B, B se produit simplement avant C, et C est séquencé avant D, ou
(12.4) il y a une évaluation B telle que A se produit fortement avant B, et B se produit fortement avant D.
[Remarque: officieusement, si A se produit fortement avant B, alors A semble être évalué avant B dans tous les contextes. Se produit fortement avant d'exclure les opérations de consommation. - note de fin]
Pourquoi «cela se produit-il avant» a été introduit? Intuitivement, quelle est sa différence et sa relation avec "qui se passe avant"?
Que signifie «A semble être évalué avant B dans tous les contextes» dans la note?
(Remarque: la motivation de cette question sont les commentaires de Peter Cordes sous cette réponse .)
Projet de devis standard supplémentaire (merci à Peter Cordes)
Ordre et cohérence [atomics.order] / 4
Il existe un seul ordre total S sur toutes les opérations memory_order :: seq_cst, y compris les clôtures, qui satisfait les contraintes suivantes. Premièrement, si A et B sont des opérations memory_order :: seq_cst et A se produit fortement avant B, alors A précède B dans S. Deuxièmement, pour chaque paire d'opérations atomiques A et B sur un objet M, où A est ordonné par cohérence avant B, les quatre conditions suivantes doivent être remplies par S:
(4.1) si A et B sont tous deux des opérations memory_order :: seq_cst, alors A précède B dans S; et
(4.2) si A est une opération memory_order :: seq_cst et B se produit avant une clôture memory_order :: seq_cst Y, alors A précède Y dans S; et
(4.3) si une clôture memory_order :: seq_cst X se produit avant que A et B ne soient une opération memory_order :: seq_cst, alors X précède B dans S; et
(4.4) si une barrière memory_order :: seq_cst X se produit avant A et B se produit avant une barrière memory_order :: seq_cst Y, alors X précède Y dans S.
seq_cst
, dans Atomics 31.4 Ordre et cohérence: 4 . Ce n'est pas dans la norme C ++ 17 n4659 , où 32.4 - 3 définit l'existence d'un seul ordre total d'opérations seq_cst cohérent avec l'ordre «se produit avant» et les ordres de modification pour tous les emplacements affectés ; le «fortement» a été ajouté dans un projet ultérieur.atexit()
dans un thread etexit()
dans un autre, il ne suffit pas que les initialiseurs portent uniquement une dépendance basée sur la consommation uniquement parce que les résultats diffèrent de if aexit()
été invoqué par le même thread. Une de mes réponses les plus anciennes concernait cette différence.exit()
. N'importe quel thread peut tuer l'intégralité du programme en quittant, ou le thread principal peut quitter enreturn
-ing. Cela entraîne l'appel desatexit()
gestionnaires et la mort de tous les threads quoi qu'ils fassent.Réponses:
Préparez-vous aussi pour "arrive simplement avant"! Jetez un œil à cet instantané actuel de cppref https://en.cppreference.com/w/cpp/atomic/memory_order
Il semble que "se produit simplement avant" soit ajouté en C ++ 20.
Donc, Simply-HB et HB sont les mêmes, sauf pour la façon dont ils gèrent les opérations de consommation. Voir HB
En quoi diffèrent-ils en matière de consommation? Voir Inter-Thread-HB
Une opération dont la dépendance est ordonnée (c'est-à-dire qui utilise la libération / la consommation) est HB mais pas nécessairement Simply-HB.
Consommer est plus détendu qu'acquérir, donc si je comprends bien, HB est plus détendu que Simply-HB.
Une opération de libération / consommation ne peut donc pas être Strongly-HB.
La version / acquisition peut être HB et Simply-HB (car la version / acquisition se synchronise avec) mais n'est pas nécessairement Strongly-HB. Parce que Strongly-HB dit spécifiquement que A doit se synchroniser avec B ET être une opération séquentielle cohérente.
Tous les contextes: tous les threads / tous les CPU voient (ou "finiront par s'entendre") le même ordre. C'est la garantie d'une cohérence séquentielle - un ordre global de modification totale de toutes les variables. Les chaînes d'acquisition / libération garantissent uniquement l'ordre de modification perçu pour les threads participant à la chaîne. Les threads en dehors de la chaîne sont théoriquement autorisés à voir un ordre différent.
Je ne sais pas pourquoi Strongly-HB et Simply-HB ont été introduits. Peut-être pour aider à clarifier comment fonctionner autour de consommer? Strongly-HB a de belles propriétés - si un thread observe A se produit fortement avant B, il sait que tous les threads observeront la même chose.
L'histoire de consommer:
Paul E. McKenney est responsable de consommer dans les standards C et C ++. Consume garantit un ordre entre l'affectation du pointeur et la mémoire vers laquelle il pointe. Il a été inventé à cause du DEC Alpha. Le DEC Alpha pouvait déréférencer spéculativement un pointeur, il avait donc également une barrière de mémoire pour empêcher cela. Le DEC Alpha n'est plus fabriqué et aucun processeur n'a aujourd'hui ce comportement. Consommer est destiné à être très détendu.
la source
mo_consume
est destiné à tirer parti de l'ordre de dépendance des données sur les CPU réels et à formaliser le fait que le compilateur ne peut pas rompre la dépendance aux données via la prédiction de branche. Par exemple,int *p = load();
tmp = *p;
le compilateur pourrait interrompre l'introductionif(p==known_address) tmp = *known_address; else tmp=*p;
s'il avait des raisons de s'attendre à ce qu'une certaine valeur de pointeur soit commune. C'est légal pour se détendre mais pas consommer.