1
votes

L'ordre de mémoire détendu peut-il être utilisé pour observer une condition?

std::atomic<bool> b;

void f()
{
    // block A
    if(b.load(std::memory_order_relaxed))
    {
        // block B
    }
    // block C
}

void g()
{
    // block B
    b.store(true, std::memory_order_release);
}
Theoretically block B should only be executed if the atomic load returns true,
but is it possible that part of block B can get reordered before the load? store with release memory order guarantees that all operations on block B are a visible side effect, but does that still apply if the load is a relaxed operation?

7 commentaires

AFAIK seule la charge elle-même est garantie d'être atomique. Le reste peut encore être réorganisé, etc.


Je ne vois pas comment le titre du message correspond à son corps.


Copie possible de ordre et visibilité du modèle de mémoire?


@Evg Désolé, je ne suis pas très doué pour décrire brièvement les problèmes techniques. Pouvez-vous proposer un meilleur titre?


Eh bien, vous n'avez ni try_lock ni mutex . Je suppose que vous vous demandez si vous pouvez simuler ceux-ci de manière sans verrou en utilisant atomic . Dans ce cas, le titre correspond bien, bien que légèrement incertain.


J'ai supprimé la mention déroutante des mutex dans le titre. Si vous le ramenez, veuillez ajouter une section sur l'applicabilité de votre Q aux mutex.


Peut-être que vous auriez pu avoir une seule section de code "bloc B", au lieu de deux.


3 Réponses :


1
votes

La principale chose dont vous devriez vous préoccuper est l'accès à la ressource que vous verrouillez avec ce "mutex". Sans sémantique d'acquisition / libération, votre thread peut ne pas voir les modifications apportées à cette ressource par l'autre thread. Autrement dit, votre lecture de ces données et l'écriture de l'autre thread constituent une course aux données sans sémantique d'acquisition / de libération.

Vous ne devriez utiliser des ordres de mémoire assouplis que si tout ce que vous voulez faire est d'accéder à la valeur atomique elle-même, sans aucune question sur ce qui se passe dans le monde par rapport à la valeur de cet atomique.


16 commentaires

La sémantique assouplie est fondamentalement malsaine car elle peut montrer une valeur d'un «futur» non spécifié. (Eh bien, toute la sémantique de MT semble également malsaine, mais cette partie est plus manifestement malsaine.)


@curiousguy: arrêteriez-vous de faire peur à propos de mo_relaxed ? Oui, diverses optimisations au moment de la compilation peuvent réorganiser une charge relâchée après d'autres choses dans la source. Il en va de même pour certains effets d'exécution théoriques, comme la prédiction de valeur, ce qu'aucun processeur réel ne fait, et il est peu probable que cela se produise de si tôt. mo_relaxed n'est pas fondamentalement défectueux, ce n'est tout simplement pas approprié si vous vous souciez de commander. toute autre chose (à moins que la réorganisation ne soit limitée par des clôtures proches ou des opérations acq / rel). Un magasin de sortie force une charge relâchée à se produire avant le magasin.


@Nicol: Dans la terminologie C ++, "course aux données" a une signification technique d'une condition de concurrence sur des variables non atomiques. Je crois comprendre qu'une course non-UB de variété de jardin sur des atomiques détendus ne devrait pas être appelée une "course aux données", juste une "condition de course". Ou si le programme ne fonctionne que pour un gagnant mais pas pour l'autre, cela pourrait être un "bug de course". Ce n'est pas UB dans C ++ 11 pour que deux threads non synchronisés utilisent tous les deux des magasins en écriture seule sur une variable atomique, et la valeur que vous trouverez dépendra de celle qui est arrivée en dernier. (Détendu, relâché ou seq-cst n'a pas d'importance pour cela.)


@PeterCordes: Mais ce n'est pas ce dont je parle. Quoi que fasse le "bloc B", si cela est en effet destiné à être une forme de "mutex" (qui était le phrasé original de l'OP avant que Curiousguy ne le modifie), alors cela signifie qu'il y a un autre thread qui a accédé à de la mémoire, et la récupération atomique est destiné à empêcher l'accès simultané, à communiquer que le thread d'écriture est terminé et que le thread de lecture peut lire la valeur. Si vous n'utilisez qu'un ordre de mémoire relaxé , alors les lectures dans le "bloc B" provoqueront une course de données avec l'autre thread. C'est ce que le mutex est censé empêcher.


Oups, oui, je pensais à l'atome lui-même, ou à d'autres données atomiques du bloc B (ce qui aurait du sens si le producteur ne dispose d'aucun mécanisme permettant au consommateur de signaler qu'il est prêt pour une autre mise à jour, donc vous ne pouvez pas garantir qu'il ne fait pas déjà plus de mises à jour après avoir vu un indicateur).


@PeterCordes Non, l'ensemble de la norme C ++ n'a aucun sens, comme je l'ai expliqué dans un Q maintenant supprimé (supprimé car il soulevait un problème sérieux). Vous ne pouvez pas avoir de "voyage dans le temps" et étape par étape sémantique et UB dans un PL. Cela n'a aucun sens. Ce n'est pas seulement détendu qui est cassé, c'est toute l'idée de «réorganiser».


@PeterCordes: Veuillez ne pas répondre à curiousguy. Il vaut mieux le laisser tranquille; il publiera des commentaires sur à peu près n'importe quoi dans la balise language-avocat à propos du C ++ juste pour promouvoir ses divers bogues à propos du standard C ++, et il les défendra ad nausium sans véritables citations du standard. J'ai appris qu'il vaut mieux l'ignorer. Ou du moins, si vous avez à lui répondre, emportez-le quelque part afin que je ne reçoive pas de ping pour cela.


@curiousguy: La façon dont C ++ définit son modèle de mémoire (en termes de séquences de synchronisation et de libération) est très différente de la façon dont les ISA matériels définissent le leur (en termes de réorganisations autorisées pour les magasins du même cœur, ou de différents observateurs sur différents cœurs (IRIW), et des trucs comme ça.) Il peut donc être assez difficile de rester droit dans l'abstrait, au lieu de simplement en termes de la façon dont une implémentation C ++ spécifique mappe différents ordres de charges, de magasins et de RMW à des instructions / barrières sur un ISA donné.


@curiousguy: le comportement en une seule étape des instructions C ++ dans la machine abstraite C ++ n'est pas un effet secondaire observable que la règle as-if doit préserver. C'est pourquoi l'optimisation du compilateur et la réorganisation de l'exécution sont autorisées à le détruire même dans un programme sans UB. Ou pour le dire autrement, pourquoi les compilateurs n'ont pas besoin de tout renforcer en seq-cst. (\ @Nicol: ok, désolé pour le bruit. Arrêtez-vous ici.) Si vous ne comprenez pas comment utiliser des ordres de mémoire plus faibles, ne les utilisez pas. seq-cst est recommandé par de nombreux utilisateurs de C ++, sauf si vous avez besoin des performances que vous pouvez obtenir en l'affaiblissant.


@PeterCordes non, ce que je veux dire, c'est que rien n'est sûr; vous ne pouvez pas détruire une variable atomique, ou peut-être même la construire (aucun n'a de paramètre d'ordre de mémoire). Aucun programme MT n'est défini en raison du mélange de sémantiques UB et non CS. Ppl danse autour du problème comme la danse autour de chacun de mes Q sur ce site, quand ils ne les suppriment pas (c'est arrivé deux fois!).


@curiousguy Même si je ne suis pas d'accord avec votre analyse concernant l'atomique détendu, je ne pense pas que la question à laquelle vous continuez de faire référence aurait dû être supprimée. Les gens ont le droit de poser des questions, même si cette question remet en question la norme et est donc considérée comme controversée par beaucoup. Je ne sais pas pourquoi il a été supprimé (votes fermés ou autre), mais peut-être pouvez-vous demander à un modérateur de le remettre en ligne.


@LWimsey: Je ne peux pas dire pourquoi il a été supprimé, mais il a été fermé parce qu'il était incroyablement large, se résumant essentiellement à «expliquer et justifier le modèle de mémoire C ++». Dans la spécification, le modèle de mémoire de threading se compose de 5 pages, et c'est sans regarder les différents éléments de la bibliothèque standard qui ajoutent au nombre de pages. Et cela n'inclut pas les diverses affirmations bizarres et inexactes sur la spécification. Dans l'ensemble, la question n'était pas formulée comme une recherche de la vérité mais plutôt comme une tentative de propager le FUD; c'était vraiment un "quand as-tu arrêté de battre ta femme?" genre de question.


@NicolBolas Maintenant, la formulation était mauvaise? Pourquoi personne ne peut-il expliquer comment prouver que n'importe quel programme MT (même à un seul thread!) Est correct: comment raisonner sur un programme. C'était un simple Q. Même pas toute la norme aurait besoin d'être couverte, juste tout ce qui n'est pas spécifié en terme de SC. Votre accusation "FUD" est juste, eh bien, FUD.


@NicolBolas Je dois admettre que je ne me souviens pas des détails de cette question, mais il me semblait que quelqu'un l'avait supprimée dans une réponse émotionnelle (ce qui, je pense, est une mauvaise raison). Mais je conviens qu'il existe certainement des cas qui justifient la clôture et / ou la suppression de questions.


@curiousguy: comme je l'ai commenté plus tôt, la réorganisation est limitée par des relations de synchronisation. Pour un nouveau fil, son point de création est (je pense) un point où il se synchronise avec son créateur. Les constructeurs atomiques n'ont pas besoin d'être atomiques eux-mêmes car toute façon de passer l'adresse à un autre thread établit une relation qui se produit avant où la construction est terminée avant que quiconque puisse la regarder. Souvenez-vous que la réorganisation ne provient que du PDV des autres threads; un seul thread voit toujours ses propres opérations dans l'ordre du programme. C'est trivial d'écrire un simple MT sans UB


(Désolé pour le bruit supplémentaire, Nicol. J'ai vraiment fini ici après cette déclaration générale selon laquelle les règles C ++ s'emboîtent vraiment de manière cohérente pour permettre de raisonner sur les programmes atomiques et MT détendus, et / ou d'utiliser acq / rel atomics pour permettre une synchronisation sûre entre différents threads qui lisent / écrivent des variables non atomiques sans UB. Ce n'est pas le lieu pour expliquer pourquoi en détail, mais je ne pouvais pas laisser une telle affirmation de type théorie du complot rester incontestée. )



3
votes

Il est recommandé par Intel d'effectuer une charge relâchée avant d'essayer de verrouiller dans Profiter des boucles de veille de puissance et de performance :

ATTEMPT_AGAIN:
    if (!acquire_lock())
    {
        /* Spin on pause max_spin_count times before backing off to sleep */
        for(int j = 0; j < max_spin_count; ++j)
        {
            /* pause intrinsic */
            _mm_pause();
            if (read_volatile_lock()) // <--- relaxed load
            {
                if (acquire_lock())
                {
                    goto PROTECTED_CODE;
                }
            }
        }
        /* Pause loop didn't work, sleep now */
        Sleep(0);
        goto ATTEMPT_AGAIN;
    }
PROTECTED_CODE:
    get_work();
    release_lock();
    do_work();

Acquérir_lock utilise la sématique d'acquisition afin que la charge relâchée ne soit pas réordonnée acquire_lock.

Notez, cependant, qu'il essaie d'abord de se verrouiller sans condition avant de faire une boucle d'attente occupée avec la charge relâchée.


1 commentaires

Ah, vous pouvez donc effectuer une opération détendue pour voir si l'atome a été écrit et si c'est le cas, chargez-le à nouveau avec l'ordre d'acquisition pour éviter la réorganisation avant? C'est très utile, merci.



3
votes

Vous avez deux bloc B dans votre exemple. Je parle de celui de la fonction de chargement void f () .

Est-il possible qu'une partie du bloc B puisse être réorganisée avant le chargement?

Oui. Le compilateur pourrait extraire des charges du corps if () et les faire avant le b.load . Cela est susceptible de se produire si les blocs B et C lisent la même variable non atomique.

Et il existe des mécanismes réels qui créeront cette réorganisation même sans réorganisation au moment de la compilation:

Plus précisément, spéculation de branche (c'est-à-dire prédiction de branche + exécution spéculative dans le désordre) permettra au CPU de commencer à exécuter le bloc B avant même b.load () commence.

Vous ne pouvez pas dépendre de la "causalité" ou de tout type de raisonnement comme "il faudrait connaître le résultat de b.load () avant de pouvoir savoir quoi exécuter ensuite".

Ou un compilateur pourrait potentiellement faire une conversion if de if () en code sans branche, s'il n'y a pas de magasins dans le bloc B. Ensuite, il pourrait bien évidemment réorganiser avec des charges non atomiques , ou d'autres charges détendues ou acquises, qui étaient dans les blocs B et C.

(Souvenez-vous que acq / rel sont des barrières à sens unique.)


Un raisonnement comme celui-ci (basé sur ce que les vrais compilateurs et processeurs peuvent faire) peut être utile pour prouver que quelque chose n'est pas sûr. Mais faites attention à ne pas aller dans l'autre sens: un raisonnement basé sur "sûr sur le compilateur que je connais" ne signifie pas toujours "sûr dans le C ++ ISO portable" .

Parfois, "en toute sécurité sur le compilateur que je connais" est plus ou moins suffisant, mais il est difficile de séparer cela de "fonctionne sur la compilation que je connais", où une future version du compilateur ou un changement de source apparemment sans rapport pourrait casser quelque chose.

Alors essayez toujours de raisonner sur l'ordre de la mémoire en termes de modèle de mémoire C ++, ainsi qu'en termes de comment il peut compiler efficacement pour un ISA qui vous tient à cœur (par exemple x86 fortement ordonné). Comme vous pourriez voir que la relaxation permettrait une réorganisation au moment de la compilation qui est réellement utile dans votre cas.


0 commentaires