Quand utiliser volatile avec multi threading?


131

S'il y a deux threads accédant à une variable globale, de nombreux didacticiels indiquent que la variable est volatile pour empêcher le compilateur de mettre en cache la variable dans un registre et qu'elle ne soit donc pas mise à jour correctement. Cependant, deux threads accédant tous les deux à une variable partagée est quelque chose qui appelle une protection via un mutex, n'est-ce pas? Mais dans ce cas, entre le verrouillage du thread et la libération du mutex, le code est dans une section critique où seul ce thread peut accéder à la variable, auquel cas la variable n'a pas besoin d'être volatile?

Alors, quel est donc l'utilité / le but de volatile dans un programme multi-thread?


3
Dans certains cas, vous ne voulez / n'avez pas besoin de protection par le mutex.
Stefan Mai

4
Parfois c'est bien d'avoir une condition de course, parfois non. Comment utilisez-vous cette variable?
David Heffernan du

3
@David: Un exemple de quand c'est "bien" d'avoir une course, s'il vous plaît?
John Dibling

6
@John Voilà. Imaginez que vous avez un thread de travail qui traite un certain nombre de tâches. Le thread de travail incrémente un compteur chaque fois qu'il termine une tâche. Le thread maître lit périodiquement ce compteur et met à jour l'utilisateur avec des nouvelles de la progression. Tant que le compteur est correctement aligné pour éviter le déchirement, il n'est pas nécessaire de synchroniser l'accès. Bien qu'il y ait une course, elle est bénigne.
David Heffernan

5
@John Le matériel sur lequel ce code s'exécute garantit que les variables alignées ne peuvent pas souffrir de déchirement. Si le travailleur met à jour n à n + 1 pendant que le lecteur lit, le lecteur ne se soucie pas de savoir s'il obtient n ou n + 1. Aucune décision importante ne sera prise car elle n'est utilisée que pour les rapports d'étape.
David Heffernan

Réponses:


168

Réponse courte et rapide : volatileest (presque) inutile pour la programmation d'applications multithread indépendante de la plateforme. Il ne fournit aucune synchronisation, il ne crée pas de barrières de mémoire, ni n'assure l'ordre d'exécution des opérations. Cela ne rend pas les opérations atomiques. Cela ne rend pas votre code comme par magie thread-safe. volatilepeut être la fonction la plus mal comprise de tout C ++. Voir ceci , ceci et cela pour plus d'informations survolatile

D'un autre côté, volatileil a une utilité qui n'est peut-être pas si évidente. Il peut être utilisé de la même manière que l'on utiliserait constpour aider le compilateur à vous montrer où vous pourriez faire une erreur en accédant à une ressource partagée d'une manière non protégée. Cette utilisation est abordée par Alexandrescu dans cet article . Cependant, cela utilise essentiellement le système de type C ++ d'une manière qui est souvent considérée comme un artifice et peut évoquer un comportement indéfini.

volatileétait spécifiquement destiné à être utilisé lors de l'interfaçage avec du matériel mappé en mémoire, des gestionnaires de signaux et l'instruction de code machine setjmp. Cela rend volatiledirectement applicable à la programmation au niveau des systèmes plutôt qu'à la programmation normale au niveau des applications.

La norme C ++ 2003 ne dit pas que volatiles'applique tout type de sémantique Acquire ou Release sur les variables. En fait, le Standard est complètement silencieux sur toutes les questions de multithreading. Cependant, des plates-formes spécifiques appliquent la sémantique Acquire and Release aux volatilevariables.

[Mise à jour pour C ++ 11]

Le C ++ 11 standard maintenant fait accusé multithreading directement dans le modèle de mémoire et le lanuage, et il fournit des installations de bibliothèque pour y faire face d'une manière plate-forme indépendante. Cependant, la sémantique de volatilen'a pas encore changé. volatilen'est toujours pas un mécanisme de synchronisation. Bjarne Stroustrup le dit dans TCPPPL4E:

Ne pas utiliser volatilesauf dans le code de bas niveau qui traite directement du matériel.

Ne supposez pas qu'il volatilea une signification particulière dans le modèle de mémoire. Ce ne est pas. Ce n'est pas - comme dans certaines langues ultérieures - un mécanisme de synchronisation. Pour obtenir la synchronisation, utilisez atomic, a mutexou a condition_variable.

[/ Mettre fin à la mise à jour]

Ce qui précède applique le langage C ++ lui-même, tel que défini par la norme 2003 (et maintenant la norme 2011). Certaines plates-formes spécifiques ajoutent cependant des fonctionnalités ou des restrictions supplémentaires à ce que volatilefait. Par exemple, en 2010 MSVC (au moins) acquérir et sémantique de sortie ne sont applicables à certaines opérations sur les volatilevariables. Depuis le MSDN :

Lors de l'optimisation, le compilateur doit maintenir l'ordre parmi les références aux objets volatils ainsi que les références à d'autres objets globaux. En particulier,

Une écriture sur un objet volatil (écriture volatile) a une sémantique Release; une référence à un objet global ou statique qui se produit avant une écriture sur un objet volatil dans la séquence d'instructions se produira avant cette écriture volatile dans le binaire compilé.

Une lecture d'un objet volatil (lecture volatile) a la sémantique Acquire; une référence à un objet global ou statique qui se produit après une lecture de mémoire volatile dans la séquence d'instructions se produira après cette lecture volatile dans le binaire compilé.

Cependant, vous pouvez prendre note du fait que si vous suivez le lien ci - dessus, il y a un débat dans les commentaires si oui ou non la sémantique acquisition / libération fait appliquer dans ce cas.


19
Une partie de moi souhaite voter contre cela en raison du ton condescendant de la réponse et du premier commentaire. «volatile is inutile» est semblable à «l'allocation manuelle de mémoire est inutile». Si vous pouvez écrire un programme multithread sans volatilecela, c'est parce que vous vous êtes tenu sur les épaules de personnes qui volatileimplémentaient des bibliothèques de threads.
Ben Jackson

20
@Ben juste parce que quelque chose remet en question vos croyances ne le rend pas condescendant
David Heffernan

39
@Ben: non, lisez ce qui volatilese passe réellement en C ++. Ce que @John a dit est correct , fin de l'histoire. Cela n'a rien à voir avec le code de l'application vs le code de la bibliothèque, ou les programmeurs omniscients "ordinaires" vs "divins". volatileest inutile et inutile pour la synchronisation entre les threads. Les bibliothèques de threads ne peuvent pas être implémentées en termes de volatile; il doit de toute façon s'appuyer sur des détails spécifiques à la plate-forme, et lorsque vous comptez sur ceux-ci, vous n'en avez plus besoin volatile.
jalf

6
@jalf: «volatile est inutile et inutile pour la synchronisation entre threads» (ce que vous avez dit) n'est pas la même chose que «volatile est inutile pour la programmation multithread» (ce que John a dit dans la réponse). Vous avez raison à 100%, mais je ne suis pas d'accord avec John (partiellement) - volatile peut toujours être utilisé pour la programmation multithread (pour un ensemble très limité de tâches)

4
@GMan: Tout ce qui est utile n'est utile que sous un certain ensemble d'exigences ou de conditions. Volatile est utile pour la programmation multithread sous un ensemble strict de conditions (et dans certains cas, peut même être mieux (pour une certaine définition de meilleur) que des alternatives). Vous dites "ignorer ceci et ..." mais le cas où volatile est utile pour le multithreading n'ignore rien. Vous avez inventé quelque chose que je n'ai jamais réclamé. Oui, l'utilité de volatile est limitée, mais il existe - mais nous pouvons tous convenir qu'il n'est PAS utile pour la synchronisation.

31

(Note de l'éditeur: en C ++ 11 volatilen'est pas le bon outil pour ce travail et a toujours UB de course aux données. Utilisez std::atomic<bool>avec des std::memory_order_relaxedcharges / magasins pour le faire sans UB. Sur de vraies implémentations, il compilera au même asm que volatile. J'ai ajouté une réponse plus détaillée, et abordant également les idées fausses dans les commentaires selon lesquelles une mémoire faiblement ordonnée pourrait être un problème pour ce cas d'utilisation: tous les processeurs du monde réel ont une mémoire partagée cohérente, donc volatilecela fonctionnera pour cela sur les implémentations C ++ réel Mais encore don. ne le fais pas.

Certaines discussions dans les commentaires semblent parler d'autres cas d'utilisation où vous auriez besoin de quelque chose de plus fort que l'atomique détendu. Cette réponse indique déjà que volatilevous ne commandez pas.)


Volatile est parfois utile pour la raison suivante: ce code:

/* global */ bool flag = false;

while (!flag) {}

est optimisé par gcc pour:

if (!flag) { while (true) {} }

Ce qui est évidemment incorrect si l'indicateur est écrit par l'autre thread. Notez que sans cette optimisation, le mécanisme de synchronisation fonctionne probablement (selon l'autre code, certaines barrières de mémoire peuvent être nécessaires) - il n'y a pas besoin d'un mutex dans le scénario 1 producteur - 1 consommateur.

Sinon, le mot-clé volatile est trop étrange pour être utilisable - il ne fournit aucune garantie d'ordre de la mémoire pour les accès volatils et non volatils et ne fournit aucune opération atomique - c'est-à-dire que vous n'obtenez aucune aide du compilateur avec un mot-clé volatile sauf la mise en cache de registre désactivée .


4
Si je me souviens bien, C ++ 0x atomic, est censé faire correctement ce que beaucoup de gens pensent (à tort) être fait par volatile.
David Heffernan

14
volatilen'empêche pas la réorganisation des accès mémoire. volatileles accès ne seront pas réorganisés les uns par rapport aux autres, mais ils ne fournissent aucune garantie sur la réorganisation par rapport aux non- volatileobjets, et donc, ils sont fondamentalement inutiles en tant que drapeaux.
jalf

14
@Ben: Je pense que vous l'avez à l'envers. La foule "volatile est inutile" repose sur le simple fait que volatile ne protège pas contre la réorganisation , ce qui signifie qu'il est totalement inutile pour la synchronisation. D'autres approches peuvent être tout aussi inutiles (comme vous le mentionnez, l'optimisation du code au moment du lien peut permettre au compilateur de jeter un coup d'œil dans le code que vous supposiez que le compilateur traiterait comme une boîte noire), mais cela ne corrige pas les lacunes de volatile.
jalf

15
@jalf: Voir l'article d'Arch Robinson (lié ailleurs sur cette page), 10e commentaire (par "Spud"). Fondamentalement, la réorganisation ne change pas la logique du code. Le code publié utilise l'indicateur pour annuler une tâche (plutôt que pour signaler que la tâche est terminée), donc peu importe si la tâche est annulée avant ou après le code (par exemple: while (work_left) { do_piece_of_work(); if (cancel) break;}si l'annulation est réorganisée dans la boucle, la logique est toujours valide.J'avais un morceau de code qui fonctionnait de la même manière: si le thread principal veut se terminer, il définit le drapeau pour d'autres threads, mais ce n'est pas le cas ...

15
... importe si les autres threads effectuent quelques itérations supplémentaires de leurs boucles de travail avant de se terminer, à condition que cela se produise raisonnablement peu de temps après la définition de l'indicateur. Bien sûr, c'est la SEULE utilisation à laquelle je peux penser et c'est plutôt une niche (et peut ne pas fonctionner sur les plates-formes où l'écriture dans une variable volatile ne rend pas le changement visible pour d'autres threads, bien que sur au moins x86 et x86-64 cela travaux). Je ne conseillerais certainement à personne de le faire sans une très bonne raison, je dis simplement qu'une déclaration générale comme "volatile n'est JAMAIS utile dans le code multithread" n'est pas correcte à 100%.

16

En C ++ 11, normalement ne jamais utiliser volatilepour le threading, uniquement pour MMIO

Mais TL: DR, ça "fonctionne" un peu comme atomic avec mo_relaxeddu matériel avec des caches cohérents (c'est-à-dire tout); il suffit d'empêcher les compilateurs de conserver les variables dans les registres. atomicn'a pas besoin de barrières de mémoire pour créer l'atomicité ou la visibilité inter-thread, uniquement pour faire attendre le thread actuel avant / après une opération pour créer un ordre entre les accès de ce thread à différentes variables. mo_relaxedn'a jamais besoin de barrières, il suffit de charger, stocker ou RMW.

Pour les atomics roll-your-own avec volatile(et inline-asm pour les barrières) dans le mauvais vieux temps avant C ++ 11 std::atomic, volatilec'était le seul bon moyen de faire fonctionner certaines choses . Mais cela dépendait de beaucoup d'hypothèses sur le fonctionnement des implémentations et n'était jamais garanti par aucune norme.

Par exemple, le noyau Linux utilise toujours ses propres atomiques roulés à la main avec volatile , mais ne prend en charge que quelques implémentations C spécifiques (GNU C, clang et peut-être ICC). C'est en partie à cause des extensions GNU C et de la syntaxe et de la sémantique asm en ligne, mais aussi parce que cela dépend de certaines hypothèses sur le fonctionnement des compilateurs.

C'est presque toujours le mauvais choix pour les nouveaux projets; vous pouvez utiliser std::atomic(avec std::memory_order_relaxed) pour qu'un compilateur émette le même code machine efficace que vous pourriez avec volatile. std::atomicavec des mo_relaxedobsolètes volatileà des fins de filetage. (sauf peut-être pour contourner les bogues d'optimisation manqués avec atomic<double>certains compilateurs .)

L'implémentation interne std::atomicdes compilateurs grand public (comme gcc et clang) ne s'utilise pas seulement en volatileinterne; les compilateurs exposent directement les fonctions intégrées de charge atomique, de stockage et de RMW. (par exemple, les modules internes GNU C__atomic qui fonctionnent sur des objets "simples".)


Volatile est utilisable dans la pratique (mais ne le faites pas)

Cela dit, volatileest utilisable en pratique pour des choses comme un exit_nowindicateur sur toutes (?) Les implémentations C ++ existantes sur des processeurs réels, en raison du fonctionnement des processeurs (caches cohérents) et des hypothèses partagées sur la façon dont volatiledevraient fonctionner. Mais pas grand-chose d'autre et n'est pas recommandé. Le but de cette réponse est d'expliquer comment les processeurs existants et les implémentations C ++ fonctionnent réellement. Si vous ne vous souciez pas de cela, tout ce que vous devez savoir est std::atomicqu'avec mo_relaxed obsolètes volatilepour le threading.

(La norme ISO C ++ est assez vague à ce sujet, disant simplement que les volatileaccès doivent être évalués strictement selon les règles de la machine abstraite C ++, et non optimisés. Étant donné que les implémentations réelles utilisent l'espace d'adressage mémoire de la machine pour modéliser l'espace d'adressage C ++, cela signifie que les volatilelectures et les affectations doivent être compilées pour charger / stocker des instructions pour accéder à la représentation d'objet en mémoire.)


Comme le souligne une autre réponse, un exit_nowindicateur est un cas simple de communication inter-thread qui ne nécessite aucune synchronisation : il ne publie pas que le contenu du tableau est prêt ou quelque chose comme ça. Juste un magasin qui est remarqué rapidement par une charge non optimisée dans un autre thread.

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

Sans volatile ni atomique, la règle as-if et l'hypothèse de non-course aux données UB permettent à un compilateur de l'optimiser en asm qui ne vérifie le drapeau qu'une seule fois , avant d'entrer (ou non) dans une boucle infinie. C'est exactement ce qui se passe dans la vraie vie pour les vrais compilateurs. (Et généralement, optimisez beaucoup do_stuffparce que la boucle ne se termine jamais, donc tout code ultérieur qui aurait pu utiliser le résultat n'est pas accessible si nous entrons dans la boucle).

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

Le programme multithreading bloqué en mode optimisé mais s'exécute normalement en -O0 est un exemple (avec une description de la sortie asm de GCC) de la façon dont cela se produit exactement avec GCC sur x86-64. Aussi la programmation MCU - pauses d'optimisation C ++ O2 en boucle sur electronics.SE montre un autre exemple.

Nous souhaitons normalement des optimisations agressives qui permettent au CSE de sortir les charges des boucles, y compris pour les variables globales.

Avant C ++ 11, il y volatile bool exit_nowavait un moyen de faire fonctionner ce travail comme prévu (sur les implémentations C ++ normales). Mais dans C ++ 11, la course aux données UB s'applique toujours, volatiledonc il n'est pas réellement garanti par la norme ISO de fonctionner partout, même en supposant des caches cohérents HW.

Notez que pour les types plus larges, volatilene donne aucune garantie d'absence de déchirure. J'ai ignoré cette distinction ici boolparce que ce n'est pas un problème sur les implémentations normales. Mais c'est aussi une partie de la raison pour laquelle il volatileest toujours soumis à l'UB de course aux données au lieu d'être équivalent à l'atome relaxé.

Notez que "comme prévu" ne signifie pas que le thread en cours exit_nowattend que l'autre thread se termine. Ou même qu'il attend que le exit_now=truemagasin volatil soit même globalement visible avant de poursuivre les opérations ultérieures dans ce thread. ( atomic<bool>avec la valeur par défaut, mo_seq_cstcela ferait au moins attendre avant tout chargement ultérieur de seq_cst. Sur de nombreux ISA, vous auriez juste une barrière complète après le magasin).

C ++ 11 fournit un moyen non-UB qui compile le même

Un indicateur "continuer à courir" ou "quitter maintenant" doit être utilisé std::atomic<bool> flagavecmo_relaxed

En utilisant

  • flag.store(true, std::memory_order_relaxed)
  • while( !flag.load(std::memory_order_relaxed) ) { ... }

vous donnera exactement la même chose (sans instructions de barrière coûteuses) que vous obtiendrez volatile flag.

En plus de ne pas déchirer, atomicvous donne également la possibilité de stocker dans un thread et de charger dans un autre sans UB, de sorte que le compilateur ne peut pas extraire la charge d'une boucle. (L'hypothèse de l'absence de course aux données UB est ce qui permet les optimisations agressives que nous souhaitons pour les objets non volatils non atomiques.) Cette fonctionnalité atomic<T>est à peu près la même que volatilepour les charges pures et les magasins purs.

atomic<T>faites également +=et ainsi de suite des opérations RMW atomiques (beaucoup plus chères qu'une charge atomique dans un magasin temporaire, opérez, puis un magasin atomique séparé. Si vous ne voulez pas d'un RMW atomique, écrivez votre code avec un temporaire local).

Avec la seq_cstcommande par défaut que vous obtiendrez while(!flag), cela ajoute également des garanties de commande. accès non atomiques, et à d'autres accès atomiques.

(En théorie, la norme ISO C ++ n'exclut pas l'optimisation à la compilation des atomiques. Mais en pratique, les compilateurs ne le font pas parce qu'il n'y a aucun moyen de contrôler quand cela ne serait pas correct. Il y a quelques cas où même volatile atomic<T>pas avoir un contrôle suffisant sur l'optimisation de l'atome si les compilateurs optimisent, donc pour l'instant les compilateurs ne le font pas. Voir Pourquoi les compilateurs ne fusionnent-ils pas les écritures std :: atomic redondantes? Notez que wg21 / p0062 recommande de ne pas utiliser volatile atomicdans le code actuel pour se prémunir contre l'optimisation de atomique.)


volatile fonctionne réellement pour cela sur de vrais processeurs (mais ne l'utilisez toujours pas)

même avec des modèles de mémoire faiblement ordonnés (non-x86) . Mais ne l'utilisez pas réellement, utilisez plutôt atomic<T>avec mo_relaxed!! Le but de cette section est de traiter les idées fausses sur le fonctionnement des processeurs réels, et non de justifier volatile. Si vous écrivez du code sans verrou, vous vous souciez probablement des performances. Comprendre les caches et les coûts de la communication inter-thread est généralement important pour de bonnes performances.

Les vrais processeurs ont des caches / mémoire partagée cohérents: après qu'un stockage d'un cœur devient globalement visible, aucun autre cœur ne peut charger une valeur périmée. (Voir aussi Myths Programmers Believe about CPU Caches qui en parle des volatiles Java, équivalent à C ++ atomic<T>avec l'ordre de mémoire seq_cst.)

Quand je dis load , je veux dire une instruction asm qui accède à la mémoire. C'est ce qu'un volatileaccès garantit, et ce n'est pas la même chose que la conversion lvalue-to-rvalue d'une variable C ++ non atomique / non volatile. (par exemple local_tmp = flagou while(!flag)).

La seule chose que vous devez vaincre est les optimisations à la compilation qui ne se rechargent pas du tout après la première vérification. Tout chargement + contrôle à chaque itération est suffisant, sans aucune commande. Sans synchronisation entre ce thread et le thread principal, il n'est pas significatif de parler du moment exact du magasin ou de l'ordre de la charge. autres opérations dans la boucle. Ce n'est que lorsqu'il est visible par ce fil que ce qui compte. Lorsque vous voyez l'indicateur exit_now défini, vous quittez. La latence inter-core sur un Xeon x86 typique peut être quelque chose comme 40ns entre des cœurs physiques séparés .


En théorie: threads C ++ sur du matériel sans caches cohérents

Je ne vois aucun moyen que cela puisse être efficace à distance, avec juste du C ++ ISO pur sans exiger du programmeur qu'il fasse des vidages explicites dans le code source.

En théorie, vous pourriez avoir une implémentation C ++ sur une machine qui ne serait pas comme ça, nécessitant des vidages explicites générés par le compilateur pour rendre les choses visibles à d'autres threads sur d'autres cœurs . (Ou pour les lectures de ne pas utiliser une copie peut-être périmée). Le standard C ++ ne rend pas cela impossible, mais le modèle de mémoire de C ++ est conçu pour être efficace sur des machines à mémoire partagée cohérentes. Par exemple, le standard C ++ parle même de "cohérence lecture-lecture", "cohérence écriture-lecture", etc. Une note de la norme indique même la connexion au matériel:

http://eel.is/c++draft/intro.races#19

[Remarque: 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. Cela rend effectivement la garantie de cohérence du cache fournie par la plupart du matériel disponible pour les opérations atomiques C ++. - note de fin]

Il n'y a pas de mécanisme pour qu'un releasemagasin ne se vide que lui-même et quelques plages d'adresses sélectionnées: il devrait tout synchroniser car il ne saurait pas ce que les autres threads pourraient vouloir lire si leur chargement d'acquisition voyait ce magasin de versions (formant un release-sequence qui établit une relation qui se produit avant entre les threads, garantissant que les opérations non atomiques précédentes effectuées par le thread d'écriture sont désormais sûres à lire. À moins qu'il n'écrive plus loin après le magasin de publication ...) Ou les compilateurs auraient être vraiment intelligent pour prouver que seules quelques lignes de cache avaient besoin d'être vidées.

Connexes: ma réponse sur Mov + mfence est-il sûr sur NUMA? va dans le détail sur la non-existence de systèmes x86 sans mémoire partagée cohérente. Également lié: Charge et stocke la réorganisation sur ARM pour en savoir plus sur les charges / magasins au même emplacement.

Il y a, je pense, des clusters avec une mémoire partagée non cohérente, mais ce ne sont pas des machines à image système unique. Chaque domaine de cohérence exécute un noyau séparé, vous ne pouvez donc pas exécuter les threads d'un seul programme C ++. À la place, vous exécutez des instances distinctes du programme (chacune avec son propre espace d'adressage: les pointeurs dans une instance ne sont pas valides dans l'autre).

Pour les amener à communiquer entre eux via des vidages explicites, vous utiliseriez généralement MPI ou une autre API de transmission de messages pour que le programme spécifie les plages d'adresses à vider.


Le matériel réel ne dépasse pas les std::threadlimites de cohérence du cache:

Certaines puces ARM asymétriques existent, avec un espace d'adressage physique partagé mais pas de domaines de cache partageables en interne. Donc pas cohérent. (par exemple, commentez un noyau A8 et un Cortex-M3 comme TI Sitara AM335x).

Mais différents noyaux fonctionneraient sur ces cœurs, pas une seule image système qui pourrait exécuter des threads sur les deux cœurs. Je ne connais aucune implémentation C ++ qui exécute des std::threadthreads sur les cœurs de processeur sans caches cohérents.

Pour ARM spécifiquement, GCC et clang génèrent du code en supposant que tous les threads s'exécutent dans le même domaine partageable interne. En fait, le manuel ARMv7 ISA dit

Cette architecture (ARMv7) est écrite avec l'espoir que tous les processeurs utilisant le même système d'exploitation ou hyperviseur sont dans le même domaine de partage interne

Ainsi, la mémoire partagée non cohérente entre des domaines séparés n'est qu'une chose pour une utilisation spécifique au système explicite de régions de mémoire partagée pour la communication entre différents processus sous différents noyaux.

Voir aussi cette discussion CoreCLR sur la génération de code à l'aide des dmb ishbarrières de dmb symémoire (Inner Shareable) et (System) dans ce compilateur.

J'affirme qu'aucune implémentation C ++ pour un autre ISA ne fonctionne std::threadsur des cœurs avec des caches non cohérents. Je n'ai pas de preuve qu'une telle implémentation existe, mais cela semble hautement improbable. À moins que vous ne cibliez un élément exotique spécifique de HW qui fonctionne de cette façon, votre réflexion sur les performances devrait supposer une cohérence de cache de type MESI entre tous les threads. (Utilisez de préférence atomic<T>de manière à garantir l'exactitude, cependant!)


Les caches cohérents simplifient les choses

Mais sur un système multicœur avec des caches cohérents, implémenter un release-store signifie simplement commander la validation dans le cache pour les magasins de ce thread, sans faire de vidage explicite. ( https://preshing.com/20120913/acquire-and-release-semantics/ et https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (Et une acquisition-charge signifie commander l'accès au cache dans l'autre noyau).

Une instruction de barrière de mémoire bloque simplement les charges et / ou les stockages du thread actuel jusqu'à ce que le tampon de stockage se vide; cela se produit toujours aussi vite que possible tout seul. ( Une barrière de mémoire garantit-elle que la cohérence du cache est terminée? Résout cette idée fausse). Donc, si vous n'avez pas besoin de commander, une simple visibilité rapide dans d'autres threads, mo_relaxedc'est bien. (Et c'est ainsi volatile, mais ne faites pas ça.)

Voir aussi mappages C / C ++ 11 vers les processeurs

Fait amusant: sur x86, chaque magasin asm est un magasin de versions car le modèle de mémoire x86 est essentiellement seq-cst plus un tampon de stockage (avec transfert de magasin).


Semi-liés re: tampon de stockage, visibilité globale et cohérence: C ++ 11 garantit très peu. La plupart des vrais ISA (sauf PowerPC) garantissent que tous les threads peuvent se mettre d'accord sur l'ordre d'apparition de deux magasins par deux autres threads. (Dans la terminologie formelle du modèle de mémoire de l'architecture informatique, ils sont "atomiques à copies multiples").

Une autre idée fausse est que les instructions asm de clôture de la mémoire sont nécessaires pour vider le tampon de stockage pour d' autres noyaux pour voir nos magasins du tout . En fait, le tampon de stockage essaie toujours de se vider (commettre dans le cache L1d) aussi vite que possible, sinon il se remplirait et bloquerait l'exécution. Ce que fait une barrière / clôture complète est de bloquer le thread actuel jusqu'à ce que le tampon de magasin soit vidé , de sorte que nos charges ultérieures apparaissent dans l'ordre global après nos magasins précédents.

(Le modèle de mémoire asm fortement ordonné volatilede x86 signifie que sur x86 peut finir par vous rapprocher de mo_acq_rel, sauf que la réorganisation au moment de la compilation avec des variables non atomiques peut toujours se produire. Mais la plupart des non-x86 ont des modèles de mémoire faiblement ordonnés volatileet relaxedsont à peu près aussi faible comme le mo_relaxedpermet.)


Les commentaires ne sont pas destinés à une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew

2
Excellente rédaction. C'est exactement ce que je cherchais (donnant tous les faits) au lieu d'une déclaration générale qui dit simplement "utiliser atomique au lieu de volatile pour un seul drapeau booléen partagé global".
bernie

2
@bernie: J'ai écrit ceci après avoir été frustré par des affirmations répétées selon lesquelles ne pas utiliser atomicpourrait conduire à différents threads ayant des valeurs différentes pour la même variable dans le cache . / facepalm. Dans le cache, non, dans les registres du processeur oui (avec des variables non atomiques); Les processeurs utilisent un cache cohérent. Je souhaite que les autres questions sur SO ne soient pas pleines d'explications pour atomiccette propagation des idées fausses sur le fonctionnement des processeurs. (Parce que c'est une chose utile à comprendre pour des raisons de performances, et aide également à expliquer pourquoi les règles atomiques ISO C ++ sont écrites telles qu'elles sont.)
Peter Cordes

-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

Une fois, un intervieweur qui pensait également que la volatilité était inutile a fait valoir avec moi que l'optimisation ne poserait aucun problème et faisait référence à différents cœurs ayant des lignes de cache séparées et tout cela (ne comprenait pas vraiment à quoi il faisait exactement référence). Mais ce morceau de code compilé avec -O3 sur g ++ (g ++ -O3 thread.cpp -lpthread), il montre un comportement indéfini. Fondamentalement, si la valeur est définie avant la vérification while, cela fonctionne bien et sinon, elle entre dans une boucle sans se soucier de récupérer la valeur (qui a été modifiée par l'autre thread). Fondamentalement, je crois que la valeur de checkValue n'est récupérée qu'une seule fois dans le registre et n'est jamais vérifiée à nouveau sous le plus haut niveau d'optimisation. S'il est défini sur true avant la récupération, cela fonctionne bien et sinon, il entre dans une boucle. Veuillez me corriger si je me trompe.


4
Qu'est-ce que cela a à voir avec volatile? Oui, ce code est UB - mais c'est UB avec volatileaussi.
David Schwartz

-2

Vous avez besoin de volatiles et éventuellement de verrouillage.

volatile indique à l'optimiseur que la valeur peut changer de manière asynchrone, donc

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

lira le drapeau à chaque fois dans la boucle.

Si vous désactivez l'optimisation ou rendez chaque variable volatile, un programme se comportera de la même manière mais plus lentement. volatile signifie simplement «Je sais que vous venez de le lire et de savoir ce qu'il dit, mais si je dis lisez-le, lisez-le.

Le verrouillage fait partie du programme. Donc, au fait, si vous implémentez des sémaphores, ils doivent entre autres être volatils. (Ne l'essayez pas, c'est difficile, il faudra probablement un petit assembleur ou le nouveau truc atomique, et c'est déjà fait.)


1
Mais n'est-ce pas, et le même exemple dans l'autre réponse, une attente occupée et donc quelque chose à éviter? S'il s'agit d'un exemple artificiel, y a-t-il des exemples réels qui ne sont pas artificiels?
David Preston

7
@Chris: L'attente occupée est parfois une bonne solution. En particulier, si vous prévoyez de ne devoir attendre que quelques cycles d'horloge, cela entraîne beaucoup moins de frais généraux que l'approche beaucoup plus lourde consistant à suspendre le thread. Bien sûr, comme je l'ai mentionné dans d'autres commentaires, des exemples tels que celui-ci sont imparfaits car ils supposent que les lectures / écritures sur le drapeau ne seront pas réorganisées par rapport au code qu'il protège, et aucune garantie de ce type n'est donnée, et donc , volatilen'est pas vraiment utile même dans ce cas. Mais l'attente occupée est une technique parfois utile.
jalf

3
@richard Oui et non. La première moitié est correcte. Mais cela signifie seulement que le processeur et le compilateur ne sont pas autorisés à réorganiser les variables volatiles les unes par rapport aux autres. Si je lis une variable volatile A, puis que je lis une variable volatile B, alors le compilateur doit émettre du code qui est garanti (même avec la réorganisation du processeur) pour lire A avant B.Mais il ne donne aucune garantie sur tous les accès aux variables non volatiles . Ils peuvent être réorganisés autour de votre lecture / écriture volatile très bien. Donc, à moins que vous ne
rendiez

2
@ ctrl-alt-delor: Ce n'est pas ce que volatilesignifie "pas de réorganisation". Vous espérez que cela signifie que les magasins deviendront globalement visibles (pour d'autres threads) dans l'ordre du programme. C'est ce atomic<T>que vous offre memory_order_releaseou seq_cstvous donne. Mais vous donne volatile seulement la garantie de ne pas réorganiser à la compilation : chaque accès apparaîtra dans l'asm dans l'ordre du programme. Utile pour un pilote de périphérique. Et utile pour l'interaction avec un gestionnaire d'interruption, un débogueur ou un gestionnaire de signal sur le noyau / thread actuel, mais pas pour interagir avec d'autres cœurs.
Peter Cordes

1
volatileen pratique, cela suffit pour vérifier un keep_runningindicateur comme vous le faites ici: les vrais processeurs ont toujours des caches cohérents qui ne nécessitent pas de vidage manuel. Mais il n'y a aucune raison de recommander volatileplus atomic<T>avec mo_relaxed; vous aurez le même asm.
Peter Cordes
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.