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) volatile
dans 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 foo
est 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 foo
doit 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?
Réponses:
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 fiervolatile
seuls .Cependant, les primitives que nous aurions à utiliser pour les propriétés restantes fournissent également celles qui le
volatile
font, c'est donc effectivement inutile.Pour les accès thread-safe aux données partagées, nous avons besoin d'une garantie que:
volatile
variable 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 ?volatile
garantit le premier point. Il garantit également qu'aucune réorganisation ne se produit entre les différentes lectures / écritures volatiles . Tousvolatile
les 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 quivolatile
est 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'volatile
objet 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 àvolatile
ceux.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 supprimervolatile
complètement le qualificatif.Depuis C ++ 11, les variables atomiques (
std::atomic<T>
) nous donnent toutes les garanties pertinentes.la source
volatile
ne sont pas assez fortes pour être utiles.volatile
pour ê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.volatile
c'est toujours inutile pour la programmation multi-thread. (Sauf sous Visual Studio, où volatile est l'extension de la barrière mémoire.)Vous pouvez également considérer cela dans la documentation du noyau Linux .
la source
volatile
c'est également inutile. Dans tous les cas, le comportement "appel à une fonction dont le corps n'est pas visible" sera correct.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.
la source
mutex_lock
(et toute autre fonction de la bibliothèque) peut modifier l'état de la variable indicateur.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 ++volatile
n'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.volatile
est 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 autrevolatile
.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 avoirNon 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
volatile
qu'à faire des garanties supplémentaires sur les opérations ordinaires .Maintenant, vous avez quelque chose comme ça:
my_shared_flag
n'a pas besoin d'être volatile, même si elle ne peut pas être mise en cache, car&
opérateur).pthread_mutex_lock
est une fonction de bibliothèque.pthread_mutex_lock
acquiert d'une manière ou d'une autre cette référence.pthread_mutex_lock
modifie l'indicateur partagé !volatile
, bien que significatif dans ce contexte, est étranger.la source
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é):
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):
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.
peut encore être optimisé pour
si l'optimiseur est assez intelligent (il ne change pas la sémantique du code).
la source
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
la source
La FAQ comp.programming.threads a une explication classique de Dave Butenhof:
M. Butenhof couvre une grande partie du même terrain dans ce post usenet :
Tout cela s'applique également au C ++.
la source
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.
la source
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.
la source
volatile
qu'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 levolatile
travail selon les besoins sape l'objectif d'avoir une norme.