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

165

Comme démontré dans cette réponse que j'ai récemment publiée, je semble confus au sujet de l'utilité (ou de son absence) volatiledans les contextes de programmation multi-thread.

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

Donc, si vous avez un int global foo, et fooest lu par un thread et défini de manière atomique par un autre thread (probablement en utilisant une instruction machine appropriée), le thread de lecture voit cette situation de la même manière qu'il voit une variable modifiée par un gestionnaire de signal ou modifié par une condition matérielle externe et foodoit donc être déclaré volatile(ou, pour les situations multithread, accessible avec une charge délimitée par la mémoire, ce qui est probablement une meilleure solution).

Comment et où ai-je tort?

Michael Ekstrand
la source
7
Tout ce que volatile fait est de dire que le compilateur ne doit pas mettre en cache l'accès à une variable volatile. Cela ne dit rien sur la sérialisation de cet accès. Cela a été discuté ici, je ne sais combien de fois, et je ne pense pas que cette question va ajouter quoi que ce soit à ces discussions.
4
Et encore une fois, une question qui ne le mérite pas, et qui a été posée ici à plusieurs reprises avant d'être votée. Voulez-vous s'il vous plaît arrêter de faire cela.
14
@neil J'ai cherché d'autres questions et j'en ai trouvé une, mais aucune explication existante que j'ai vue n'a déclenché ce dont j'avais besoin pour vraiment comprendre pourquoi je me trompais. Cette question a suscité une telle réponse.
Michael Ekstrand
1
Pour une étude approfondie sur ce que les processeurs font avec les données (via leurs caches), consultez: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot
1
@curiousguy C'est ce que je voulais dire par "pas le cas en C", où il peut être utilisé pour écrire sur des registres matériels, etc., et n'est pas utilisé pour le multithreading comme il est couramment utilisé en Java.
Monstieur

Réponses:

213

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

Cependant, les primitives que nous aurions à utiliser pour les propriétés restantes fournissent également celles qui le volatilefont, c'est donc 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 stockera pas simplement la valeur dans un registre à la place et reportera la mise à jour de la mémoire principale à bien plus tard)
  • qu'aucune réorganisation n'a lieu. Supposons que nous utilisions une volatilevariable comme indicateur pour indiquer si certaines données sont prêtes à être lues ou non. Dans notre code, nous définissons simplement le drapeau après avoir préparé les données, donc tout semble bien. Mais que se passe-t-il si les instructions sont réorganisées de sorte que le drapeau soit défini en premier ?

volatilegarantit le premier point. Il garantit également qu'aucune réorganisation ne se produit entre les différentes lectures / écritures volatiles . Tous volatileles accès à la mémoire auront lieu dans l'ordre dans lequel ils sont spécifiés. C'est tout ce dont nous avons besoin pour ce qui volatileest prévu: manipuler des registres d'E / S ou du matériel mappé en mémoire, mais cela ne nous aide pas dans le code multithread où l' volatileobjet est souvent utilisé uniquement pour synchroniser l'accès aux données non volatiles. Ces accès peuvent encore être réorganisés par rapport à volatileceux.

La solution pour empêcher la réorganisation est d'utiliser une barrière mémoire , qui indique à la fois au compilateur et au processeur qu'aucun accès mémoire ne peut être réorganisé à ce stade . Le fait de placer de telles barrières autour de notre accès aux variables volatiles garantit que même les accès non volatils ne seront pas réorganisés entre les accès volatils, ce qui nous permet d'écrire du code thread-safe.

Cependant, les barrières de mémoire garantissent également que toutes les lectures / écritures en attente sont exécutées lorsque la barrière est atteinte, de sorte qu'elle nous donne efficacement tout ce dont nous avons besoin par elle-même.volatile inutile. Nous pouvons simplement supprimer volatilecomplètement le qualificatif.

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

jalf
la source
5
@jbcreix: De quel «il» parlez-vous? Barrières volatiles ou mémoire? Dans tous les cas, la réponse est à peu près la même. Ils doivent tous deux travailler à la fois au niveau du compilateur et du processeur, car ils décrivent le comportement observable du programme - ils doivent donc s'assurer que le processeur ne réorganise pas tout, modifiant le comportement qu'il garantit. Mais vous ne pouvez actuellement pas écrire de synchronisation de thread portable, car les barrières de mémoire ne font pas partie du C ++ standard (elles ne sont donc pas portables) et volatilene sont pas assez fortes pour être utiles.
jalf
4
Un exemple MSDN le fait et affirme que les instructions ne peuvent pas être réorganisées après un accès volatile: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW
27
@OJW: Mais le compilateur de Microsoft se redéfinit volatilepour être une barrière de mémoire complète (empêchant la réorganisation). Cela ne fait pas partie de la norme, vous ne pouvez donc pas vous fier à ce comportement dans le code portable.
jalf
4
@Skizz: non, c'est là qu'intervient la partie "magie du compilateur" de l'équation. Une barrière mémoire doit être comprise à la fois par le CPU et le compilateur. Si le compilateur comprend la sémantique d'une barrière mémoire, il sait éviter de telles astuces (ainsi que réorganiser les lectures / écritures à travers la barrière). Et heureusement, le compilateur ne comprend la sémantique d'une barrière de mémoire, donc en fin de compte , tout cela fonctionne. :)
jalf
13
@Skizz: Les threads eux-mêmes sont toujours une extension dépendante de la plate-forme avant C ++ 11 et C11. À ma connaissance, chaque environnement C et C ++ qui fournit une extension de thread fournit également une extension «barrière de mémoire». Quoi qu'il en soit, volatilec'est toujours inutile pour la programmation multi-thread. (Sauf sous Visual Studio, où volatile est l'extension de la barrière mémoire.)
Nemo
49

Vous pouvez également considérer cela dans la documentation du noyau Linux .

Les programmeurs C ont souvent pris volatile pour signifier que la variable pouvait être modifiée en dehors du thread d'exécution actuel; par conséquent, ils sont parfois tentés de l'utiliser dans le code du noyau lorsque des structures de données partagées sont utilisées. En d'autres termes, ils sont connus pour traiter les types volatils comme une sorte de variable atomique facile, ce qu'ils ne sont pas. L'utilisation de volatile dans le code du noyau n'est presque jamais correcte; ce document explique pourquoi.

Le point clé à comprendre en ce qui concerne la volatilité est que son but est de supprimer l'optimisation, ce qui n'est presque jamais ce que l'on veut vraiment faire. Dans le noyau, il faut protéger les structures de données partagées contre les accès simultanés indésirables, ce qui est une tâche très différente. Le processus de protection contre la concurrence indésirable permettra également d'éviter presque tous les problèmes liés à l'optimisation d'une manière plus efficace.

Comme volatile, les primitives du noyau qui sécurisent l'accès simultané aux données (verrous rotatifs, mutex, barrières mémoire, etc.) sont conçues pour empêcher une optimisation indésirable. S'ils sont utilisés correctement, il ne sera pas nécessaire d'utiliser également volatile. Si volatile est toujours nécessaire, il y a presque certainement un bogue dans le code quelque part. Dans le code du noyau correctement écrit, volatile ne peut servir qu'à ralentir les choses.

Considérez un bloc typique de 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 the_lock est maintenu. Tout autre code qui pourrait vouloir jouer avec ces données attendra sur le verrou. Les primitives de verrouillage tournant agissent comme des barrières de mémoire - elles sont explicitement écrites pour ce faire - ce qui signifie que les accès aux données ne seront pas optimisés entre elles. Ainsi, le compilateur peut penser qu'il sait ce qu'il y aura dans shared_data, mais l'appel spin_lock (), puisqu'il agit comme une barrière mémoire, le forcera à oublier tout ce qu'il sait. Il n'y aura aucun problème d'optimisation avec les 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 l'accès à shared_data dans la section critique, quand nous savons que personne d'autre ne peut travailler avec. Tant que le verrou est maintenu, shared_data n'est pas volatile. Lorsqu'il s'agit de données partagées, un verrouillage approprié rend la volatilité inutile - et potentiellement dangereuse.

La classe de stockage volatile était à l'origine destinée aux registres d'E / S mappés en mémoire. Au sein du noyau, les accès aux registres doivent également être protégés par des verrous, mais on ne veut pas non plus que le compilateur «optimise» les accès aux registres dans une section critique. Mais, au sein du noyau, les accès à la mémoire d'E / S se font toujours via les fonctions d'accès; l'accès direct à la mémoire d'E / S via des pointeurs est mal vu et ne fonctionne pas sur toutes les architectures. Ces accesseurs sont écrits pour empêcher une optimisation indésirable, donc, encore une fois, volatile n'est pas nécessaire.

Une autre situation où l'on pourrait être tenté d'utiliser volatile est lorsque le processeur est occupé à attendre la valeur d'une variable. La bonne façon d'effectuer une attente occupée est:

while (my_variable != what_i_want)
    cpu_relax();

L'appel cpu_relax () peut réduire la consommation d'énergie du processeur ou céder la place à un double processeur hyperthread; il se trouve également qu'il sert de barrière mémoire, donc, encore une fois, volatile n'est pas nécessaire. Bien sûr, l'attente occupée est généralement un acte antisocial pour commencer.

Il existe encore quelques rares situations où volatile a du sens dans le noyau:

  • Les fonctions d'accesseur mentionnées ci-dessus peuvent utiliser volatile sur les architectures où l'accès direct à la mémoire d'E / S fonctionne. Essentiellement, chaque appel d'accès devient une petite section critique en soi et garantit que l'accès se déroule comme prévu par le programmeur.

  • Le code d'assemblage en ligne qui change la mémoire, mais qui n'a pas d'autres effets secondaires visibles, risque d'être supprimé par GCC. L'ajout du mot-clé volatile aux instructions asm empêchera cette suppression.

  • La variable jiffies est spéciale en ce qu'elle peut avoir une valeur différente à chaque fois qu'elle est référencée, mais elle peut être lue sans aucun verrouillage spécial. Les jiffies peuvent donc être volatiles, mais l'ajout d'autres variables de ce type est fortement désapprouvé. Jiffies est considéré comme un problème d '«héritage stupide» (les mots de Linus) à cet égard; le réparer serait plus difficile que cela ne vaut la peine.

  • Les pointeurs vers des structures de données dans une mémoire cohérente qui pourraient être modifiées par des périphériques d'E / S peuvent, parfois, être légitimement volatils. Une mémoire tampon en anneau utilisée par une carte réseau, où cette carte change de pointeurs pour indiquer quels descripteurs ont été traités, est un exemple de ce type de situation.

Pour la plupart des codes, aucune des justifications ci-dessus pour volatile ne s'applique. En conséquence, l'utilisation de volatile est susceptible d'être considérée comme un bogue et apportera un examen supplémentaire au code. Les développeurs qui sont tentés d'utiliser volatile devraient prendre du recul et réfléchir à ce qu'ils essaient vraiment d'accomplir.

Communauté
la source
3
@curiousguy: Oui. Voir également gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html .
Sebastian Mach
1
Le spin_lock () ressemble à un appel de fonction normal. Ce qui est spécial, c'est que le compilateur le traitera spécialement pour que le code généré "oublie" toute valeur de shared_data qui a été lue avant le spin_lock () et stockée dans un registre afin que la valeur doive être lue à nouveau dans le do_something_on () après le spin_lock ()?
Syncopée
1
@underscore_d Mon point est que je ne peux pas dire d'après le nom de la fonction spin_lock () qu'elle fait quelque chose de spécial. Je ne sais pas ce qu'il y a dedans. En particulier, je ne sais pas ce qui est dans l'implémentation qui empêche le compilateur d'optimiser les lectures ultérieures.
Syncopée le
1
Syncopated a un bon point. Cela signifie essentiellement que le programmeur doit connaître l'implémentation interne de ces "fonctions spéciales" ou au moins être très bien informé de leur comportement. Cela soulève des questions supplémentaires, telles que - ces fonctions spéciales sont-elles standardisées et garanties de fonctionner de la même manière sur toutes les architectures et tous les compilateurs? Existe-t-il une liste de ces fonctions disponibles ou au moins existe-t-il une convention pour utiliser les commentaires de code pour signaler aux développeurs que la fonction en question protège le code contre une «optimisation»?
JustAMartin
1
@Tuntable: Une statique privée peut être touchée par n'importe quel code, via un pointeur. Et son adresse est prise. Peut-être que l'analyse du flux de données est capable de prouver que le pointeur ne s'échappe jamais, mais c'est en général un problème très difficile, super-linéaire en taille de programme. Si vous avez un moyen de garantir qu'aucun alias n'existe, alors déplacer l'accès à travers un verrou tournant devrait en fait être correct. Mais si aucun alias n'existe, volatilec'est également inutile. Dans tous les cas, le comportement "appel à une fonction dont le corps n'est pas visible" sera correct.
Ben Voigt
11

Je ne pense pas que vous vous trompez - volatile est nécessaire pour garantir que le thread A verra la valeur changer, si la valeur est modifiée par autre chose que le thread A. Si je comprends bien, volatile est fondamentalement un moyen de dire le compilateur "ne cachez pas cette variable dans un registre, assurez-vous plutôt de toujours la lire / écrire à partir de la mémoire RAM à chaque accès".

La confusion vient du fait que la volatilité n'est pas suffisante pour mettre en œuvre un certain nombre de choses. En particulier, les systèmes modernes utilisent plusieurs niveaux de mise en cache, les processeurs multicœurs modernes effectuent des optimisations sophistiquées au moment de l'exécution, et les compilateurs modernes effectuent des optimisations sophistiquées au moment de la compilation, et tout cela peut entraîner divers effets secondaires. commande de la commande que vous attendez si vous regardiez simplement le code source.

Donc, la volatilité est très bien, tant que vous gardez à l'esprit que les changements «observés» dans la variable volatile peuvent ne pas se produire au moment exact où vous pensez qu'ils le seront. Plus précisément, n'essayez pas d'utiliser des variables volatiles comme moyen de synchroniser ou d'ordonner les opérations entre les threads, car cela ne fonctionnera pas de manière fiable.

Personnellement, mon utilisation principale (seulement?) Pour l'indicateur volatile est un booléen "pleaseGoAwayNow". Si j'ai un thread de travail qui boucle en continu, je lui demanderai de vérifier le booléen volatil à chaque itération de la boucle et de quitter si le booléen est toujours vrai. Le thread principal peut ensuite nettoyer en toute sécurité le thread de travail en définissant le booléen sur true, puis en appelant pthread_join () pour attendre que le thread de travail disparaisse.

Jeremy Friesner
la source
2
Votre indicateur booléen est probablement dangereux. Comment garantissez-vous que le worker termine sa tâche et que l'indicateur reste dans la portée jusqu'à ce qu'il soit lu (s'il est lu)? C'est un travail pour les signaux. Volatile est bon pour implémenter des verrous spin simples si aucun mutex n'est impliqué, car la sécurité des alias signifie que le compilateur suppose mutex_lock(et toute autre fonction de la bibliothèque) peut modifier l'état de la variable indicateur.
Potatoswatter
6
Évidemment, cela ne fonctionne que si la nature de la routine du thread de travail est telle qu'il est garanti de vérifier périodiquement le booléen. L'indicateur volatile-bool-flag est garanti de rester dans la portée car la séquence d'arrêt de thread se produit toujours avant que l'objet qui contient le volatile-boolean ne soit détruit, et la séquence d'arrêt de thread appelle pthread_join () après avoir défini le booléen. pthread_join () bloquera jusqu'à ce que le thread de travail soit parti. Les signaux ont leurs propres problèmes, en particulier lorsqu'ils sont utilisés en conjonction avec le multithreading.
Jeremy Friesner
2
Le thread de travail n'est pas garanti de terminer son travail avant que le booléen ne soit vrai - en fait, il sera presque certainement au milieu d'une unité de travail lorsque le booléen est défini sur true. Mais cela n'a pas d'importance quand le thread de travail termine son unité de travail, car le thread principal ne va rien faire sauf le blocage à l'intérieur de pthread_join () jusqu'à ce que le thread de travail se termine, dans tous les cas. Ainsi, la séquence d'arrêt est bien ordonnée - le bool volatile (et toute autre donnée partagée) ne sera libéré qu'après le retour de pthread_join (), et pthread_join () ne reviendra pas tant que le thread de travail n'aura pas disparu.
Jeremy Friesner
10
@Jeremy, vous avez raison dans la pratique mais théoriquement cela pourrait encore casser. Sur un système à deux cœurs, un cœur exécute constamment votre thread de travail. L'autre noyau définit la valeur booléenne sur true. Cependant, il n'y a aucune garantie que le noyau du thread de travail verra jamais ce changement, c'est-à-dire qu'il ne s'arrêtera jamais même s'il a répété la vérification du booléen. Ce comportement est autorisé par les modèles de mémoire c ++ 0x, java et c #. En pratique, cela ne se produirait jamais car le thread occupé insère très probablement une barrière de mémoire quelque part, après quoi il verra le changement du booléen.
deft_code
4
Prenez un système POSIX, utilisez une politique de planification en temps réel SCHED_FIFO, une priorité statique plus élevée que les autres processus / threads du système, suffisamment de cœurs, devrait être parfaitement possible. Sous Linux, vous pouvez spécifier que le processus en temps réel peut utiliser 100% du temps CPU. Ils ne changeront jamais de contexte s'il n'y a pas de thread / processus de priorité plus élevée et ne seront jamais bloqués par les E / S. Mais le fait est que C / C ++ volatilen'est pas destiné à appliquer une sémantique de partage / synchronisation de données appropriée. Je trouve que rechercher des cas particuliers pour prouver qu'un code incorrect peut parfois fonctionner est un exercice inutile.
FooF
7

volatileest utile (bien qu'insuffisant) pour implémenter 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 manière typique de la programmation multithread n'est pas de protéger chaque variable partagée au niveau de la machine, mais plutôt d'introduire des variables de garde qui guident le déroulement 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;

Non seulement cela encapsule la «partie dure», mais c'est fondamentalement nécessaire: C n'inclut pas les opérations atomiques nécessaires pour implémenter un mutex; il n'a volatilequ'à faire des garanties supplémentaires sur les opérations ordinaires .

Maintenant, vous avez quelque chose comme ça:

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, même si elle ne peut pas être mise en cache, car

  1. Un autre fil y a accès.
  2. Cela signifie qu'une référence à celui-ci doit avoir été prise à un moment donné (avec l' &opérateur).
    • (Ou une référence a été prise à une structure contenante)
  3. pthread_mutex_lock est une fonction de bibliothèque.
  4. Cela signifie que le compilateur ne peut pas dire s'il pthread_mutex_lockacquiert d'une manière ou d'une autre cette référence.
  5. Cela signifie que le compilateur doit supposer que pthread_mutex_lockmodifie l'indicateur partagé !
  6. La variable doit donc être rechargée depuis la mémoire. volatile, bien que significatif dans ce contexte, est étranger.
Potatoswatter
la source
6

Votre compréhension est vraiment fausse.

La propriété des variables volatiles est que "les lectures et les écritures sur 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 que ce n'est pas la propriété que nous voulons pour quoi que ce soit de thread-safe.

Par exemple, un compteur thread-safe serait juste (code de type noyau linux, je ne connais 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 cela ne relierait pas l'accès au code voisin (par exemple, l'ajout d'un élément à la liste que le compteur compte). Certes, vous n'avez pas besoin de voir le compteur incrémenté en dehors de votre programme, et les optimisations sont toujours souhaitables, par exemple.

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

peut encore être optimisé pour

atomically {
  counter+=2;
}

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

jpalecek
la source
6

Pour que vos données soient cohérentes dans un environnement simultané, vous devez appliquer deux conditions:

1) Atomicité, c'est-à-dire si je lis ou écris des données en mémoire, ces données sont lues / écrites en un seul passage et ne peuvent pas être interrompues ou contestées en raison, par exemple, d'un changement de contexte

2) Cohérence, c'est-à-dire que l'ordre des opérations de lecture / écriture doit être considéré comme le même entre plusieurs environnements simultanés - que ce soit les threads, les machines, etc.

volatile ne correspond à aucun de ce qui précède - ou plus particulièrement, la norme c ou c ++ sur la façon dont volatile doit se comporter n'inclut ni l'un ni l'autre.

C'est encore pire en pratique, car certains compilateurs (tels que le compilateur Intel Itanium) tentent d'implémenter un élément de comportement sécurisé d'accès simultané (c'est-à-dire en garantissant des clôtures de mémoire) mais il n'y a pas de cohérence entre les implémentations du compilateur et de plus le standard ne l'exige pas de la mise en œuvre en premier lieu.

Marquer une variable comme volatile signifie simplement que vous forcez à vider la valeur vers et depuis la mémoire à chaque fois, ce qui, dans de nombreux cas, ralentit simplement votre code car vous avez essentiellement réduit les performances de votre cache.

c # et java AFAIK corrigent cela en faisant adhérer volatile à 1) et 2), mais on ne peut pas en dire autant pour les compilateurs c / c ++, alors faites-le comme bon vous semble.

Pour une discussion plus approfondie (mais non impartiale) sur le sujet, lisez ceci

zebrabox
la source
3
+1 - l'atomicité garantie était un autre élément de ce qui me manquait. Je supposais que le chargement d'un int est atomique, de sorte que volatile empêchant la réorganisation a fourni la solution complète du côté lecture. Je pense que c'est une hypothèse décente sur la plupart des architectures, mais ce n'est pas une garantie.
Michael Ekstrand
Quand les lectures et écritures individuelles en mémoire sont-elles interruptibles et non atomiques? Y a-t-il un avantage?
batbrat le
5

La FAQ comp.programming.threads a une explication classique de Dave Butenhof:

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

Je suis toutefois préoccupé par les cas où le compilateur et la bibliothèque de threads remplissent leurs spécifications respectives. Un compilateur C conforme peut allouer globalement une variable partagée (non volatile) à un registre qui est sauvegardé et restauré au fur et à mesure que le processeur est passé de thread à thread. Chaque thread aura sa propre valeur privée pour cette variable partagée, ce qui n'est pas ce que nous voulons d'une variable partagée.

Dans un certain sens, cela est vrai, si le compilateur en sait suffisamment sur les portées respectives de la variable et des fonctions pthread_cond_wait (ou pthread_mutex_lock). En pratique, la plupart des compilateurs n'essaieront pas de conserver des copies de registre des données globales lors d'un appel à une fonction externe, car il est trop difficile de savoir si la routine peut d'une manière ou d'une autre avoir accès à l'adresse des données.

Donc oui, il est vrai qu'un compilateur qui se conforme strictement (mais de manière très agressive) à ANSI C pourrait ne pas fonctionner avec plusieurs threads sans volatile. Mais quelqu'un ferait mieux de le réparer. Parce que tout SYSTÈME (c'est-à-dire, de manière pragmatique, une combinaison de noyau, de bibliothèques et de compilateur C) qui ne fournit pas les garanties de cohérence de la mémoire POSIX ne se conforme pas au standard POSIX. Période. Le système NE PEUT PAS exiger que vous utilisiez des variables volatiles sur des variables partagées pour un comportement correct, car POSIX exige uniquement que les fonctions de synchronisation POSIX soient nécessaires.

Donc, si votre programme s'arrête parce que vous n'avez pas utilisé volatile, c'est un BUG. Ce n'est peut-être pas un bogue en C, ni un bogue dans la bibliothèque de threads, ni un bogue dans le noyau. Mais c'est un bogue SYSTÈME, et un ou plusieurs de ces composants devront travailler pour le corriger.

Vous ne voulez pas utiliser volatile, car, sur n'importe quel système où cela fait une différence, cela coûtera beaucoup plus cher qu'une variable non volatile appropriée. (ANSI C nécessite des «points de séquence» pour les variables volatiles à chaque expression, alors que POSIX ne les requiert que lors des opérations de synchronisation - une application threadée gourmande en calcul verra beaucoup plus d'activité mémoire en utilisant volatile, et, après tout, c'est l'activité mémoire qui vous ralentit vraiment.)

/ --- [Dave Butenhof] ----------------------- [[email protected]] --- \
| Digital Equipment Corporation 110, chemin Spit Brook ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Mieux vivre grâce à la concurrence] ---------------- /

M. Butenhof couvre une grande partie du même terrain dans ce post usenet :

L'utilisation de «volatile» n'est pas suffisante pour assurer une bonne visibilité de la mémoire ou une synchronisation entre les threads. L'utilisation d'un mutex est suffisante, et, sauf en recourant à diverses alternatives de code machine non portables, (ou des implications plus subtiles des règles de mémoire POSIX qui sont beaucoup plus difficiles à appliquer en général, comme expliqué dans mon post précédent), un mutex est NÉCESSAIRE.

Par conséquent, comme Bryan l'a expliqué, l'utilisation de volatile ne fait rien d'autre qu'empêcher le compilateur de faire des optimisations utiles et souhaitables, ne fournissant aucune aide pour rendre le code "thread-safe". Vous êtes bien entendu invité à déclarer tout ce que vous voulez comme «volatile» - c'est un attribut de stockage ANSI C légal, après tout. Ne vous attendez pas à ce qu'il résout les problèmes de synchronisation des threads pour vous.

Tout cela s'applique également au C ++.

Tony Delroy
la source
Le lien est rompu; il ne semble plus indiquer ce que vous vouliez citer. Sans le texte, c'est une sorte de réponse dénuée de sens.
jww
3

C'est tout ce que fait "volatile": "Hey compilateur, cette variable pourrait changer A TOUT MOMENT (sur n'importe quel tick d'horloge) même s'il n'y a AUCUNE INSTRUCTIONS LOCALES agissant dessus. NE PAS mettre en cache cette valeur dans un registre."

C'est ça. Il indique au compilateur que votre valeur est, eh 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 du compilateur qui mettront en cache silencieusement une valeur dans un registre qui est intrinsèquement dangereuse pour EVER cache.

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

Zack Yezek
la source
3

Selon mon ancien standard C, «ce qui constitue un accès à un objet de type volatile qualifié est défini par l'implémentation» . Ainsi, les rédacteurs du compilateur C auraient pu choisir d'avoir un accès thread-safe "volatile" signifie "dans un environnement multi-processus" . Mais ils ne l'ont pas fait.

Au lieu de cela, les opérations requises pour sécuriser les threads d'une section critique dans un environnement de mémoire partagée multi-processus multi-cœur ont été ajoutées en tant que nouvelles fonctionnalités définies par l'implémentation. Et, libérés de l'exigence selon laquelle «volatile» fournirait un accès atomique et un ordre d'accès dans un environnement multi-processus, les rédacteurs du compilateur ont donné la priorité à la réduction de code par rapport à la sémantique «volatile» historique dépendante de l'implémentation.

Cela signifie que des choses comme les sémaphores «volatiles» autour de sections de code critiques, qui ne fonctionnent pas sur du nouveau matériel avec de nouveaux compilateurs, ont peut-être déjà fonctionné avec d'anciens compilateurs sur du vieux matériel, et les anciens exemples ne sont parfois pas faux, juste vieux.

David
la source
Les anciens exemples exigeaient que le programme soit traité par des compilateurs de qualité adaptés à la programmation de bas niveau. Malheureusement, les compilateurs «modernes» ont pris le fait que la norme ne les oblige pas à traiter les «volatils» de manière utile comme une indication que le code qui les obligerait à le faire est cassé, plutôt que de reconnaître que la norme ne effort pour interdire les implémentations qui sont conformes mais de qualité si faible qu'elles sont inutiles, mais ne tolère en aucun cas les compilateurs de mauvaise qualité mais conformes qui sont devenus populaires
supercat
Sur la plupart des plates-formes, il serait assez facile de reconnaître ce volatilequ'il faudrait faire pour permettre d'écrire un système d'exploitation d'une manière qui dépend du matériel mais qui est indépendante du compilateur. Exiger que les programmeurs utilisent des fonctionnalités dépendant de l'implémentation plutôt que de faire le volatiletravail selon les besoins sape l'objectif d'avoir une norme.
supercat du