172 votes

Pourquoi le volatile ne pas jugé utile en multithread C ou C++ est programmation ?

Comme démontré dans cette réponse que j'ai posté, j'ai l'impression d'être confus au sujet de l'utilité (ou l'absence) d' volatile en multi-thread contexte de programmation.

Ma compréhension est ceci: toutes les fois qu'une variable peut être modifiée à l'extérieur du flux de contrôle d'un morceau de code d'accès, cette variable doit être déclarée volatile. Les gestionnaires de signaux I/O registres, et les variables modifiées par un autre thread constituent de telles situations.

Donc, si vous avez un mondial int foo, et foo est lu par un thread et de définir automatiquement par un autre thread (probablement à l'aide d'une instruction machine), le thread de lecture, voit dans cette situation de la même manière, il voit une variable tordu par un gestionnaire de signal ou modifié par un matériel externe condition et, par conséquent foo doit être déclarée volatile (ou, pour multithread situations, accessible avec la mémoire clôturé la charge, ce qui est probablement une meilleure solution).

Comment et où ai-je tort?

225voto

jalf Points 142628

Le problème avec volatile dans un contexte multithread est qu'il ne fournit pas toutes les garanties dont nous avons besoin. Il possède quelques propriétés dont nous avons besoin, mais pas tous d'entre eux, de sorte que nous ne pouvons pas compter sur volatile seul.

Cependant, les primitives que nous aurions à utiliser pour le restant propriétés fournissent également ceux qui volatile , de sorte qu'il est effectivement inutile.

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

  • la lecture/écriture qui se passe réellement (que le compilateur ne sera pas seulement de stocker la valeur dans un registre, au lieu et à reporter la mise à jour de la mémoire principale jusqu'à ce que bien plus tard).
  • qu'aucune réorganisation a lieu. Supposons que nous utilisons volatile variable comme un drapeau pour indiquer si oui ou non certaines données est prêt à être lu. Dans notre code, nous avons tout simplement mis le drapeau après la préparation des données, de sorte que tous les regards de l'amende. Mais que faire si les instructions sont réorganisées afin de l'indicateur est défini en premier?

volatile garantit le premier point. Il garantit également qu'aucune réorganisation se produit entre les différents volatiles des lectures/écritures. Tous volatile accès à la mémoire se fera dans l'ordre dans lequel ils sont spécifiés. C'est tout ce dont nous avons besoin pour ce qu' volatile est destinée aux: manipuler les I/O de registres ou de matériel mappé en mémoire, mais il ne nous aide pas dans le code multithread, où l' volatile objet est souvent utilisé pour synchroniser l'accès à des données non volatile. Ces accès peuvent toujours être réorganisées par rapport à l' volatile .

La solution pour éviter que la réorganisation est d'utiliser une barrière de mémoire, qui indique à la fois pour le compilateur et le CPU que pas d'accès à la mémoire peuvent être réorganisées à travers ce point. Placer ces barrières autour de notre volatile variable permet de garantir que même les non-volatile accède à ne pas être réorganisées à travers le volatil, ce qui nous permet d'écrire thread-safe code.

Cependant, les barrières de la mémoire aussi s'assurer que tous dans l'attente de lectures/écritures sont exécutés lorsque la barrière est atteinte, donc effectivement nous donne tout ce dont nous avons besoin par lui-même, rendant volatile inutile. On peut tout simplement supprimer l' volatile qualifier entièrement.

52voto

Mustafah Abiola Points 719

Vous pouvez également envisager ce à partir du Noyau Linux Documentation.

C programmeurs ont souvent pris des volatiles à dire que la variable peut être modifié en dehors du thread en cours d'exécution; en conséquence, ils sont parfois tentés de l'utiliser dans le code du noyau quand on le partage des structures de données sont utilisé. En d'autres termes, ils ont été connus pour traiter les types de volatiles comme une sorte de simple variable atomique, qu'ils ne le sont pas. L'utilisation de la volatilité dans le code du noyau est presque jamais correcte, ce document explique pourquoi.

Le point essentiel à comprendre à l'égard de la volatilité, c'est que son but est pour supprimer l'optimisation, ce qui n'est presque jamais ce qu'on veut vraiment n'. Dans le noyau, il convient de protéger les données partagée des structures contre indésirables d'accès simultanés, ce qui est très bien une tâche différente. L' procédure de protection contre les simultanéité permettra également d'éviter presque tous les problèmes d'optimisation d'une façon plus efficace.

Comme volatile, le noyau primitives qui rendent l'accès simultané aux données coffre-fort (spinlocks, mutex, les barrières de la mémoire, etc.) sont conçus pour empêcher indésirables d'optimisation. Si elles sont utilisées correctement, il n'y aura pas besoin d'utiliser volatile. Si volatile est toujours nécessaire, il est presque certainement un bug dans le code quelque part. Correctement écrit noyau code, la volatilité ne peut que ralentir les choses.

Prenons le cas typique de bloc 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 qui suit le verrouillage des règles, la valeur de shared_data ne peut pas changer de manière inattendue alors the_lock est tenue. Tout autre code qui pourrait envie de jouer avec les données seront en attente sur la serrure. Le spinlock primitives agissent comme des barrières de la mémoire - s'ils sont explicitement écrit pour le faire ce qui signifie que des données d'accès ne sera pas optimisé à travers eux. De sorte que le compilateur pourrait penser qu'il sait ce qui va être dans shared_data, mais l' spin_lock() l'appel, puisqu'il agit comme une barrière de mémoire, va l'obliger à oubliez tout ce qu'il sait. Il n'y aura pas de problèmes d'optimisation avec accède à ces données.

Si shared_data ont été déclarées volatile, le blocage serait encore nécessaire. Mais le compilateur pourrait également être empêché de l'optimisation de l'accès pour shared_data au sein de la section critique, quand on sait que personne d'autre ne peut travailler avec elle. Alors que le verrou est détenu, shared_data n'est pas volatile. Lorsque vous traitez avec des données partagées, bon verrouillage rend volatile inutile et potentiellement dangereux.

La volatilité de la classe de stockage était destiné à l'origine pour memory-mapped I/O les registres. Dans le noyau, le registre accède, elle aussi, doit être protégé par les verrous, mais on ne veut pas que le compilateur "l'optimisation" enregistrer accède à l'intérieur d'une section critique. Mais, dans le noyau, la mémoire des e/S les accès sont toujours fait par le biais de fonctions d'accesseur; un accès à une mémoire des e/S directement par le biais de pointeurs, c'est mal vu et ne fonctionne pas sur tous les architectures. Ces accesseurs sont écrits pour éviter les d'optimisation, de sorte que, une fois de plus, la volatilité est inutile.

Une autre situation où l'on pourrait être tenté d'utiliser le volatile est lorsque le processeur est occupé-en attente sur la valeur d'une variable. Le droit pour effectuer une attente active est:

while (my_variable != what_i_want)
    cpu_relax();

Le cpu_relax() l'appel peut réduire la consommation du PROCESSEUR ou de céder à un hyperthread twin processeur; il arrive aussi de servir de barrière de mémoire, donc, une fois de plus, la volatilité est inutile. Bien sûr, occupé-attendre, est généralement, un anti-sociale, loi sur pour commencer.

Il y a encore quelques rares cas où les volatiles de sens que dans la noyau:

  • Le ci-dessus mentionné fonctions d'accesseur peut utiliser volatile sur les architectures où direct I/O accès à la mémoire fonctionne. Essentiellement, chaque accesseur appel devient un peu de la section critique sur son propre et s'assure que l'accès se passe comme prévu par le programmeur.

  • Assembly en ligne de code qui change de la mémoire, mais qui n'a pas d'autres visible effets secondaires, risque d'être supprimé par la GCC. L'ajout de la volatilité de l' mot-clé à l'asm, les déclarations d'empêcher cette suppression.

  • Le jiffies variable est particulière 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 jiffies peut être volatile, mais l'ajout d'autres les variables de ce type est fortement désapprouvée. Jiffies est considéré comme pour être un "stupide héritage" (Linus de mots) à cet égard; en le fixant serait plus d'ennuis que cela vaut la peine.

  • Des pointeurs vers des structures de données cohérente de la mémoire qui sont susceptibles d'être modifiés par des périphériques d'e/S peuvent, parfois, être légitimement volatile. Un anneau de la mémoire tampon utilisé par une carte réseau, où que l'adaptateur change de pointeurs vers indiquer les descripteurs ont été traitées, est un exemple de ce type de situation.

Pour la plupart de code, aucune de ces justifications, le volatile s'appliquent. En tant que résultat, 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 des volatile devrait prendre du recul et de réfléchir à ce qu'ils sont vraiment à essayer pour accomplir.

13voto

Jeremy Friesner Points 16684

Je ne pense pas que vous avez tort -- volatile est nécessaire pour garantir que le fil d'Une volonté de voir la variation de la valeur, si la valeur est modifiée par quelque chose d'autre que de fil A. si je comprends bien, la volatilité est essentiellement une façon de dire au compilateur "ne cache pas cette variable dans un registre, au lieu assurez-vous de toujours lire/écrire à partir de la mémoire RAM sur tous les accès".

La confusion est parce que la volatilité n'est pas suffisante pour mettre en œuvre un certain nombre de choses. En particulier, les systèmes modernes, l'utilisation de plusieurs niveaux de mise en cache, moderne, multi-core, faire un peu de fantaisie optimisations au moment de l'exécution, et les compilateurs modernes faire un peu de fantaisie optimisations au moment de la compilation, et tout peut entraîner divers effets secondaires montrant dans un ordre différent de celui que vous attendez si vous avez seulement regardé dans 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 au moment même où vous pensez qu'ils seront. Plus précisément, n'essayez pas d'utiliser des variables volatiles comme un moyen de synchroniser ou de l'ordre des opérations dans les threads, car il ne fonctionne pas de manière fiable.

Personnellement, ma principale (seule?) l'utilisation de la volatilité de l'indicateur est comme un "pleaseGoAwayNow" booléen. Si j'ai un thread qui tourne en permanence, je vais vérifier la volatilité des booléens à chaque itération de la boucle, et de sortie si le booléen est jamais vrai. Le thread principal peut alors en toute sécurité nettoyer le thread de travail en définissant le booléen à true, et puis l'appel de pthread_join() pour attendre jusqu'à ce que le thread de travail est allé.

9voto

Potatoswatter Points 70305

volatile est utile (mais non suffisante) pour mettre en œuvre le concept de base d'un spinlock mutex, mais une fois que vous avez (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 de la garde des variables qui guide 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;

Non seulement cette encapsuler le "dur", il est fondamentalement nécessaire: C n'inclut pas les opérations atomiques nécessaires pour mettre en œuvre un mutex; il a seulement volatile pour faire de garanties supplémentaires au sujet ordinaire des opérations.

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, en dépit d'être cache, parce que

  1. Un autre thread a accès.
  2. Sens d'une référence à elle doit avoir été prise quelque temps (avec l' &opérateur).
    • (Ou une référence a été pris pour un contenant de structure)
  3. pthread_mutex_lock est une fonction de la bibliothèque.
  4. Sens le compilateur ne peut pas savoir si pthread_mutex_lock acquiert en quelque sorte que de référence.
  5. Sens le compilateur doit supposer qu' pthread_mutex_lock transforme le drapeau commun!
  6. Si la variable doit être rechargé à partir de la mémoire. volatile, tandis que les sens dans ce contexte, est superflu.

7voto

jpalecek Points 31928

Votre compréhension est vraiment mauvais.

La propriété, que la volatilité des variables de la, "lire et écrire à cette variable sont une partie de perceptible comportement du programme". Cela signifie que ce programme fonctionne (compte tenu de l'utilisation de 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 le bien que nous voulons "thread-safe" quoi que ce soit.

Par exemple, un thread-safe compteur serait juste (linux-kernel-comme le code, je ne sais pas le c++0x équivalent):

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 volatiles ne serait probablement pas aider, parce que cela ne concerne pas l'accès à la proximité de code (par exemple. l'ajout d'un élément à la liste le compteur compte). Certainement, vous n'avez pas besoin de voir le compteur incrémenté à l'extérieur de votre programme, et des optimisations sont encore souhaitables, par exemple.

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

peut encore être optimisé pour

atomically {
  counter+=2;
}

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X