Pourquoi volatile n'est-il pas considéré comme utile dans la programmation multithread C ou C++?

comme démontré dans cette réponse j'ai récemment publié, je semble être confus au sujet de l'utilité (ou l'absence de celle-ci) de volatile dans des contextes de programmation multi-threaded.

ma compréhension est la suivante: chaque fois qu'une variable peut être changée en dehors du flux de contrôle d'un morceau de code y accédant, cette variable doit être déclarée comme étant volatile . Les gestionnaires de signaux, les registres d'entrées / sorties et les variables modifiées par un autre thread constituent de telles situations.

donc, si vous avez un int global foo , et foo est lu par un thread et réglé atomiquement par un autre thread (probablement en utilisant une instruction appropriée de la machine), le thread de lecture voit cette situation de la même manière qu'il voit une variable ajustée par un gestionnaire de signal ou modifiée par une condition matérielle externe et donc foo doit être déclaré volatile (ou, pour les situations multithreaded, accédé avec la charge mémoire clôturée, qui est probablement une meilleure solution).

Comment et où ai-je tort?

145
demandé sur Community 2010-03-21 01:10:27

9 réponses

le problème avec volatile dans un contexte multithread est qu'il ne fournit pas tous les garanties dont nous avons besoin. Il a quelques propriétés dont nous avons besoin, mais pas toutes, donc nous ne pouvons pas compter sur volatile seul .

cependant, les primitives que nous devrions utiliser pour les propriétés restantes fournissent aussi celles que volatile fait, donc il est effectivement inutile.

pour les accès thread-safe aux données partagées, nous avons besoin d'une garantie que:

  • la lecture / écriture se produit réellement (que le compilateur ne va pas simplement stocker la valeur dans un registre à la place et reporter la mise à jour de la mémoire principale jusqu'à beaucoup plus tard)
  • qu'il n'y ait pas de nouvel appel d'offres. Supposons que nous utilisions une variable volatile comme indicateur pour indiquer si certaines données sont prêtes à être lues. Dans notre code, nous plaçons simplement le drapeau après avoir préparé les données, donc tout semble très bien. Mais que faire si les instructions sont réordonnées de sorte que le drapeau est mis d'abord ?

volatile ne garantit le premier point. Il garantit également qu'aucun réarrangement ne se produit entre différents volatiles lit/écrit . Tous les accès mémoire volatile se dérouleront dans l'ordre dans lequel ils sont spécifiés. C'est tout ce dont nous avons besoin pour volatile est destiné à: la manipulation des registres d'E/S ou du matériel mémoire-mappé, mais il ne nous aide pas dans le code multithreaded où l'objet volatile est souvent utilisé seulement pour synchroniser l'accès aux données non volatiles. Ces accès peuvent encore être réorganisés par rapport aux volatile .

la solution pour empêcher la réorganisation est d'utiliser une barrière de mémoire , ce qui indique à la fois au compilateur et au CPU que aucune mémoire l'accès peut être réordonné à travers ce point . Placer de telles barrières autour de notre accès variable volatile garantit que même les accès non volatiles ne seront pas réordonnés à travers l'accès volatile, nous permettant d'écrire du code sans fil.

cependant, les barrières de mémoire aussi s'assurent que toutes les lectures/écritures en attente sont exécutées lorsque la barrière est atteinte, de sorte qu'il nous donne effectivement tout ce dont nous avons besoin par lui-même, ce qui rend volatile inutile. Nous peut simplement supprimer le qualificatif volatile entièrement.

depuis C++11, Les variables atomiques ( std::atomic<T> ) nous donnent toutes les garanties pertinentes.

193
répondu jalf 2015-03-17 20:18:22

vous pourriez aussi considérer cela de la documentation du noyau Linux .

c programmeurs ont souvent pris volatile à signifier que la variable peut être changé en dehors du fil courant de l'exécution; comme un par conséquent, ils sont parfois tentés de l'utiliser dans le code du noyau quand des structures de données partagées sont utilisées. En d'autres termes, ils ont été connu pour traiter les types volatils comme une sorte de variable atomique facile, qui ils ne le sont pas. L'utilisation de volatile dans le code du noyau n'est presque jamais correct; ce document explique pourquoi.

le point clé à comprendre en ce qui concerne volatile est que son le but est de supprimer l'optimisation, ce qui n'est presque jamais veut vraiment faire. Dans le noyau, il faut protéger les données partagées structures contre Accès simultané indésirable, qui est très une tâche différente. La procédure de protection contre les la simultanéité également à éviter presque tous les problèmes d'optimisation d'une façon plus efficace.

comme volatile, les primitives du noyau qui permettent l'accès simultané à sécurité des données (spinlocks, mutex, barrières de mémoire, etc.) sont conçus pour prévenir l'optimisation indésirable. Si elles sont utilisées correctement, il n' sera pas nécessaire d'utiliser des volatiles. Si volatile est encore nécessaire, il ya presque certainement un bug dans le code quelque part. Dans code du noyau correctement écrit, volatile ne peut que ralentir les choses vers le bas.

Considérer un bloc typique du code du noyau:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

si tout le code suit les règles de verrouillage, la valeur de shared_data ne peut pas changer de façon inattendue pendant que le_lock est tenu. Tout autre code qui pourrait vouloir jouer avec les données seront en attente sur la serrure. Les primitives spinlock agissent comme des barrières de mémoire - elles sont explicitement écrit pour le faire-ce qui signifie que l'accès aux données ne sera pas optimisé à travers eux. Donc le compilateur pourrait penser qu'il sait ce qui sera dans shared_data, mais le spin_lock() l'appel, puisqu'il agit comme une mémoire barrière, va l'obliger à oublier tout ce qu'il sait. Il n'y aura pas problèmes d'optimisation avec l'accès à ces données.

si shared_data était déclaré volatile, le verrouillage serait toujours nécessaire. Mais le compilateur serait également empêché d'optimiser accès aux données partagées dans la partie critique, quand on sait que personne d'autre ne peut travailler avec ça. Pendant que la serrure est maintenue, shared_data n'est pas volatile. Lorsque vous traitez avec des données partagées, bon le verrouillage rend la volatilité inutile - et potentiellement nuisible.

la classe de stockage volatile était à l'origine destinée aux entrées/sorties cartographiées en mémoire. inscrire. Dans le noyau, les accès de Registre, aussi, devraient être protégés par des verrous, mais on ne veut pas le compilateur "l'optimisation" enregistrer accès dans une section critique. Mais, à l'intérieur de les accès à la mémoire du noyau, des e/s sont toujours effectués par l'intermédiaire d'accessor fonctions; l'accès à la mémoire des E / S directement par des pointeurs est mal vu et ne fonctionne pas sur toutes les architectures. Ces accesseurs sont écrit pour empêcher l'optimisation indésirable, donc, encore une fois, volatile est inutile.

une autre situation où l'on pourrait être tenté d'utiliser volatile est quand le processeur est occupé-il attend la valeur d'un variable. Droit la façon d'effectuer une attente chargée est:

while (my_variable != what_i_want)
    cpu_relax();

l'appel cpu_relax() peut réduire la consommation D'énergie CPU ou le rendement à un processeur twin hyperthreaded; il se trouve aussi servir de mémoire barrière, donc, encore une fois, volatile est inutile. Bien sûr, l'attente chargée est généralement un acte antisocial au départ.

il existe encore quelques rares situations où la volatilité a un sens le noyau:

  • les architectures où l'accès direct à la mémoire des E/S fonctionne. Essentiellement, chaque appel d'accesseur devient une petite section critique en soi et s'assure que l'accès se passe comme prévu par le programmeur.

  • code d'assemblage en ligne qui change la mémoire, mais qui n'a pas d'autre effets secondaires visibles, risque d'être supprimé par GCC. L'ajout de la instable les mots-clés des énoncés asm empêcheront cette suppression.

  • la variable jiffies est spéciale en ce qu'elle peut avoir une valeur différente chaque fois qu'il est référencé, mais il peut être lu sans verrouillage. Donc les jiffies peuvent être volatiles, mais l'ajout d'autres les variables de ce type sont fortement mal vues. Jiffies est considéré être un" héritage stupide " question (les mots de Linus) à cet égard; la réparation serait plus mal que ce qu'il vaut.

  • pointeurs vers des structures de données en mémoire cohérente qui pourraient être modifiées par les dispositifs D'E / S peuvent, parfois, légitimement être volatiles. Un anneau de la mémoire tampon utilisé par un adaptateur réseau, où cet adaptateur change les pointeurs pour indiquer quels descripteurs ont été traités, en est un exemple type de situation.

Pour la plupart de code, aucune de ces justifications de la volatilité des appliquer. En conséquence, l'utilisation de la volatilité est susceptible d'être considérée comme un bug et apportera un examen plus approfondi du code. Les développeurs qui sont tentés d'utiliser les volatiles devraient prendre du recul et de réfléchir à ce ils sont vraiment en train d'essayer d'accomplir.

44
répondu Muhammed Abiola 2015-04-26 03:42:44

Je ne pense pas que vous vous trompiez -- volatile est nécessaire pour garantir que le thread a verra la valeur changer, si la valeur est changée par quelque chose d'autre que le thread A. Comme je le comprends, volatile est essentiellement une façon de dire au compilateur"ne cache pas cette variable dans un registre, au lieu de s'assurer de toujours lire/écrire à partir de la mémoire RAM sur chaque accès".

la confusion est due au fait que volatile n'est pas suffisant pour mettre en œuvre un certain nombre de choses. Notamment, les systèmes modernes utilisent plusieurs niveaux de mise en cache, les CPU multi-core modernes font quelques optimisations fantaisistes à l'exécution, et les compilateurs modernes font quelques optimisations fantaisistes à la compilation, et tout cela peut entraîner divers effets secondaires apparaissant dans un ordre différent de l'ordre auquel vous vous attendez si vous venez de regarder le code source.

Si volatile est très bien, aussi longtemps que vous gardez à l'esprit que les "observés" des changements dans la volatilité de la variable ne peut pas se produire à l'heure exacte que vous pensent-ils. En particulier, n'essayez pas d'utiliser des variables volatiles comme moyen de synchroniser ou de commander des opérations entre les threads, car cela ne fonctionnera pas de manière fiable.

personnellement, mon principal (seulement?) utiliser pour le drapeau volatile est comme un booléen" pleaseGoAwayNow". Si j'ai un worker thread qui boucle en continu, je vais le faire vérifier le booléen volatile sur chaque itération de la boucle, et sortir si le booléen est jamais vrai. Le fil principal peut alors nettoyer en toute sécurité le fil ouvrier en mettant le booléen à true, puis en appelant pthread_join() pour attendre que le thread de worker soit parti.

10
répondu Jeremy Friesner 2010-03-20 22:19:57

votre compréhension est vraiment erronée.

la propriété, que les variables volatiles ont, Est "lit à partir de et écrit à cette variable font partie du comportement perceptible du programme". Cela signifie que ce programme fonctionne (avec le matériel approprié):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

le problème est, ce n'est pas la propriété que nous voulons de thread-safe quoi que ce soit.

par exemple, un compteur de thread-safe serait juste (linux-kernel-like code, ne sait pas l'équivalent C++0x):

atomic_t counter;

...
atomic_inc(&counter);

C'est atomique, sans barrière de mémoire. Vous devez les ajouter si nécessaire. L'ajout de volatile n'aiderait probablement pas, car il n'établirait pas de lien entre l'accès au code à proximité (p. ex. l'ajout d'un élément à la liste le compteur compte). Certes, vous n'avez pas besoin de voir le compteur incrémenté en dehors de votre programme, et des optimisations sont toujours souhaitables, par exemple.

atomic_inc(&counter);
atomic_inc(&counter);

peut encore être optimisé à

atomically {
  counter+=2;
}

si l'optimiseur est assez intelligent (on ne change pas la sémantique du code).

6
répondu jpalecek 2010-03-20 22:43:18

volatile est utile (bien qu'insuffisante) pour mettre en œuvre la construction de base d'un mutex spinlock, mais une fois que vous avez cela (ou quelque chose de supérieur), vous n'avez pas besoin d'un autre volatile .

la méthode typique de la programmation multithread ne consiste pas à protéger chaque variable partagée au niveau de la machine, mais plutôt à introduire des variables de garde qui guident le flux du programme. Au lieu de volatile bool my_shared_flag; vous devriez avoir

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Not ce n'est que cela encapsule la "partie dure", c'est fondamentalement nécessaire: C n'inclut pas opérations atomiques nécessaires pour mettre en œuvre un mutex; il n'a volatile pour faire des garanties supplémentaires sur opérations" ordinaires .

Maintenant vous avez quelque chose comme ceci:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag n'a pas besoin d'être volatile, bien qu'il soit uncacheable, parce que

  1. un autre thread y a accès.
  2. signifie qu'une référence à celui-ci doit avoir été prise à un moment donné (avec l'opérateur & ).
    • (ou une référence à une structure contenant)
  3. pthread_mutex_lock est une fonction de bibliothèque.
  4. signifie que le compilateur ne peut pas dire si pthread_mutex_lock acquiert d'une manière ou d'une autre cette référence.
  5. le compilateur doit supposer que pthread_mutex_lock modifie le drapeau partagé !
  6. ainsi la variable doit être rechargée de mémoire. volatile , bien que significatif dans ce contexte, est superflu.
6
répondu Potatoswatter 2010-03-20 23:23:21

pour que vos données soient cohérentes dans un environnement concurrent, vous avez besoin de deux conditions pour appliquer:

1) atomicité I. e si je lis ou écris certaines données en mémoire, alors ces données sont lues/écrites en une seule passe et ne peuvent être interrompues ou contestées en raison de E. G un commutateur de contexte

2) consistance I. e l'ordre des opérations de lecture / écriture doit être vu pour être le même entre plusieurs environnements concurrents-que les threads, machines etc

volatile fits ne correspond ni à ce qui précède, ni plus particulièrement à la norme c ou C++ sur le comportement de volatile.

c'est encore pire dans la pratique car certains compilateurs ( comme le compilateur intel Itanium ) tentent de mettre en œuvre un élément de comportement de sécurité d'accès simultané ( I. toutefois, il n'y a pas de cohérence entre les implémentations des compilateurs et, en outre, la norme ne nécessite pas cela de la mise en œuvre en premier lieu.

marquer une variable comme volatile signifie simplement que vous forcez la valeur à être évacuée de et vers la mémoire à chaque fois ce qui dans de nombreux cas ne fait que ralentir votre code comme vous avez essentiellement soufflé votre performance de cache.

c# et java autant que je sache faire la réparation en faisant des volatiles adhérer aux 1) et 2) toutefois, le même ne peut pas être dit pour les compilateurs c/c++ donc, fondamentalement, de faire avec elle comme vous le voyez équiper.

Pour un peu plus en profondeur ( mais pas impartial ) discussion sur le sujet, lire ce

6
répondu zebrabox 2010-03-21 01:28:08

comp.programmation.threads FAQ a une explication classique par Dave Butenhof:

Q56: pourquoi n'ai-je pas besoin de déclarer les variables partagées volatiles?

je suis inquiète cependant des cas où le compilateur et le bibliothèque threads remplir leurs spécifications respectives. Conforme C le compilateur peut globalement attribuer une variable partagée (non volontaire) à un registre qui est enregistré et restauré alors que le CPU est passé de fil à fil. Chaque fil aura sa propre valeur privée pour cette variable partagée, qui n'est pas ce que nous voulons d'une variable.

dans un certain sens, c'est vrai, si le compilateur en sait assez sur le champ d'application respectif de la variable et du pthread_cond_wait (ou pthread_mutex_lock). En pratique, la plupart des compilateurs ne vont pas essayer de garder des copies de données globales à travers un appel à l'externe fonction, parce qu'il est trop difficile de savoir si la routine pourrait en quelque sorte avoir accès à l'adresse de la donnée.

Donc, oui, c'est vrai qu'un compilateur qui se conforme strictement (mais très agressivement) à L'Anse c pourrait ne pas fonctionner avec plusieurs threads sans instable. Mais quelqu'un ferait mieux de le réparer. Parce que n'importe quel système (qui est, pragmatiquement, une combinaison de noyau, bibliothèques et compilateur C) qui ne fournit pas les garanties de cohérence de mémoire POSIX ne fournit pas CONFORMER à la norme POSIX. Période. Le système ne peut pas vous obliger à utiliser volatile sur les variables partagées pour un comportement correct, parce que POSIX exige seulement que les fonctions de synchronisation POSIX soient nécessaires.

donc si votre programme casse parce que vous n'avez pas utilisé volatile, c'est un BUG. Ce n'est peut-être pas un bug en C, ou un bug dans la bibliothèque threads, ou un bug dans noyau. Mais c'est un bug système, et un ou plusieurs de ces composants vous avez à travailler pour le fixer.

vous ne voulez pas utiliser volatile, parce que, sur tout système où il fait n'importe quelle différence, il sera beaucoup plus cher qu'un bon non volatile de la variable. (ANSI C exige des "points de séquence" pour les variables à chaque expression, alors que POSIX ne les requiert qu'à opérations de synchronisation -- une application filetée à forte intensité de calcul va voir beaucoup plus d'activité de mémoire en utilisant volatile, et, après tout, c'est l'activité de mémoire qui vraiment vous ralentit.)

/---[ Dave Butenhof ]-----------------------[ butenhof@zko.dec.com ]---\

/ Digital Equipment Corporation 110 Spit Brook Rd ZKO2-3 / Q18 /

| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 /

----------------- [Vivre Mieux Grâce À La Concurrence ]----------------/

M. Butenhof couvre une grande partie le même motif dans ce post usenet :

L'utilisation de "volatile" n'est pas suffisante pour assurer une bonne mémoire visibilité ou synchronisation entre les threads. L'utilisation d'un mutex est suffisant, et, sauf en recourant à diverses machines non portatives code alternatives, (ou implications plus subtiles de la mémoire POSIX les règles qui sont beaucoup plus difficiles à appliquer en général, comme expliqué dans my previous post), un mutex est nécessaire.

donc, comme Bryan a expliqué, l'utilisation de produits volatils accomplit rien que pour empêcher le compilateur de rendre utile et souhaitable optimisations, ne fournissant aucune aide que ce soit dans la fabrication de fil de code " sûr." Vous êtes les bienvenus, bien sûr, pour déclarer tout ce que vous voulez comme "volatile" -- c'est un attribut de stockage ANSI C légal, après tout. Juste ne vous attendez pas à ce que cela résolve des problèmes de synchronisation de thread pour vous.

Tout ce qui est également applicable au c++.

5
répondu Tony Delroy 2016-08-15 16:22:06

selon mon ancienne norme C, "ce qui constitue un accès à un objet de type Volatil - qualifié est la mise en œuvre-défini" ". Ainsi, les rédacteurs de C compilateur pourraient avoir choisi d'avoir" volatile "moyen " l'accès sûr de fil dans un environnement multi-processus " . Mais ils ne l'ont pas fait.

à la place, les opérations nécessaires pour rendre un thread de section critique sûr dans un multi-core multi-processus l'environnement de mémoire partagée a été ajouté en tant que nouvelles fonctionnalités définies par la mise en œuvre. Et, libérés de l'exigence que "volatile" fournirait l'accès atomique et l'ordonnancement d'accès dans un environnement multi-processus, les rédacteurs de compilateur ont donné la priorité à la réduction de code sur la sémantique "volatile" dépendante de la mise en œuvre historique.

cela signifie que des choses comme les sémaphores "volatiles" autour des sections de code critiques, qui ne fonctionnent pas sur le nouveau matériel avec de nouveaux compilateurs, pourraient Une fois avoir fonctionné avec de vieux compilateurs sur du vieux matériel, et de vieux exemples ne sont parfois pas faux, juste Vieux.

3
répondu david 2014-11-14 11:34:30

C'est tout ce que "volatile" fait: "Hé compilateur, cette variable pourrait changer à tout moment (sur n'importe quel tic-tac d'horloge) même s'il n'y a pas D'INSTRUCTIONS locales agissant sur elle. Ne cache pas cette valeur dans un registre."

C'est tout. Il indique au compilateur que votre valeur est, bien, volatile - cette valeur peut être modifiée à tout moment par une logique externe (un autre thread, un autre processus, le noyau, etc.). Il existe plus ou moins Uniquement pour supprimer les optimisations des compilateurs cela cache silencieusement une valeur dans un registre qu'il est intrinsèquement dangereux de cacher.

Vous pouvez rencontrer des articles comme "Dr Dobbs" que le ton volatile que certains panacée pour le multi-thread de programmation. Son approche n'est pas totalement dénuée de mérite, mais elle présente le défaut fondamental de rendre les utilisateurs d'un objet responsables de sa sécurité du fil, qui tend à avoir les mêmes problèmes que d'autres violations de l'encapsulation.

2
répondu Zack Yezek 2014-08-02 01:54:26