la présence de mutex aide-t-elle à se débarrasser du mot clé volatile?

J’ai une classe de locking multi-R / W qui conserve les compteurs de lecture, d’écriture et de lecture en attente et d’écriture en attente. Un mutex les garde de plusieurs threads.

Ma question est la suivante: avons-nous encore besoin que les compteurs soient déclarés volatils afin que le compilateur ne les gâche pas pendant l’optimisation?

Ou le compilateur prend-il en compte le fait que les compteurs sont gardés par le mutex.

Je comprends que le mutex est un mécanisme d’exécution pour la synchronisation et que le mot clé “volatile” est une indication de la compilation au compilateur pour faire ce qui est juste tout en faisant les optimisations.

Cordialement, -Jay.

Il y a 2 éléments fondamentalement non liés ici, qui sont toujours confondus.

  • volatil
  • fils, serrures, barrières de mémoire, etc.

volatile est utilisé pour dire au compilateur de produire du code pour lire la variable à partir de la mémoire, pas à partir d’un registre. Et pour ne pas réorganiser le code autour. En général, ne pas optimiser ou prendre des «raccourcis».

Les barrières de mémoire (fournies par mutex, verrous, etc.), telles que citées dans une autre réponse de Herb Sutter, servent à empêcher le CPU de réorganiser les demandes de mémoire en lecture / écriture, quelle que soit la manière dont le compilateur a dit de le faire. Autrement dit, n’optimisez pas, ne prenez pas de raccourcis – au niveau du processeur.

Des choses semblables, mais en réalité très différentes.

Dans votre cas, et dans la plupart des cas de locking, la raison pour laquelle volatile n’est PAS nécessaire, c’est que des appels de fonction sont effectués dans un souci de locking. c’est à dire:

Appels de fonction normaux affectant les optimisations:

external void library_func(); // from some external library global int x; int f() { x = 2; library_func(); return x; // x is reloaded because it may have changed } 

à moins que le compilateur puisse examiner library_func () et déterminer qu’il ne touche pas x, il relira x sur le retour. C’est même SANS volatile.

Filetage:

 int f(SomeObject & obj) { int temp1; int temp2; int temp3; int temp1 = obj.x; lock(obj.mutex); // really should use RAII temp2 = obj.x; temp3 = obj.x; unlock(obj.mutex); return temp; } 

Après avoir lu obj.x pour temp1, le compilateur va relire obj.x pour temp2 – PAS à cause de la magie des verrous – mais parce qu’il est incertain que lock () modifie obj. Vous pourriez probablement configurer les indicateurs de compilation pour optimiser de manière agressive (sans alias, etc.) et ainsi ne pas relire x, mais dans ce cas, une partie de votre code commencerait probablement à échouer.

Pour temp3, le compilateur (espérons-le) ne relira pas obj.x. Si pour une raison quelconque obj.x pouvait changer entre temp2 et temp3, vous utiliseriez alors volatile (et votre locking serait brisé / inutile).

Enfin, si vos fonctions lock () / unlock () étaient en quelque sorte intégrées, le compilateur pourrait peut-être évaluer le code et voir que obj.x n’est pas modifié. Mais je garantis l’une des deux choses suivantes: – le code en ligne appelle éventuellement une fonction de locking au niveau du système d’exploitation (empêchant ainsi l’évaluation) ou – vous appelez des instructions de barrière de mémoire asm (c’est-à-dire qui sont encapsulées dans des fonctions en ligne telles que __InterlockedCompareExchange) que votre compilateur reconnaîtra et ainsi éviter de réorganiser.

EDIT: PS, j’ai oublié de mentionner – pour les projets pthreads, certains compilateurs sont marqués “conformes à POSIX”, ce qui signifie, entre autres choses, qu’ils reconnaîtront les fonctions pthread_ et ne procéderont pas à de mauvaises optimisations. c’est-à-dire que même si la norme C ++ ne mentionne pas encore les threads, ces compilateurs le font (au moins de manière minimale).

Donc, réponse courte

vous n’avez pas besoin de volatile.

Extrait de l’article de Herb Sutter “Utilisez des sections critiques (de préférence des verrous) pour éliminer les courses” ( http://www.ddj.com/cpp/201804238 ):

Ainsi, pour qu’une transformation de réorganisation soit valide, elle doit respecter les sections critiques du programme en obéissant à la règle clé des sections critiques: le code ne peut pas quitter une section critique. (C’est toujours acceptable pour le code d’entrer.) Nous appliquons cette règle d’or en exigeant une sémantique symésortingque de clôture unidirectionnelle pour le début et la fin de toute section critique, illustrée par les flèches sur la figure 1:

  • La saisie d’une section critique est une opération d’acquisition ou une clôture d’acquisition implicite: le code ne peut jamais franchir la clôture, c’est-à-dire se déplacer d’un emplacement d’origine après la clôture à exécuter avant la clôture. Le code qui apparaît avant la clôture dans l’ordre du code source peut toutefois franchir la clôture vers le bas pour s’exécuter ultérieurement.
  • La sortie d’une section critique est une opération de libération ou une clôture de publication implicite: il s’agit simplement de l’exigence inverse qui veut que le code ne puisse pas traverser la clôture vers le bas, mais uniquement vers le haut. Cela garantit que tout autre fil qui voit l’écriture finale de la version verra également toutes les écritures qui la précèdent.

Donc, pour qu’un compilateur produise le code correct pour une plate-forme cible, quand une section critique est entrée et sortie (et que le terme section critique est utilisé dans son sens générique, pas nécessairement dans le sens Win32 de quelque chose protégé par une structure CRITICAL_SECTION – la section peut être protégée par d’autres objects de synchronisation), il faut suivre la sémantique correcte d’acquisition et de libération. Vous ne devriez donc pas avoir à marquer les variables partagées comme volatiles tant qu’elles ne sont accessibles que dans les sections critiques protégées.

volatile est utilisé pour informer l’optimiseur de toujours charger la valeur actuelle de l’emplacement, plutôt que de la charger dans un registre et de supposer qu’elle ne changera pas. Cela est particulièrement utile lorsque vous travaillez avec des emplacements de mémoire à double port ou pouvant être mis à jour en temps réel à partir de sources externes au thread.

Le mutex est un mécanisme d’exploitation au moment de l’exécution dont le compilateur ne sait vraiment rien. L’optimiseur n’en tiendra donc pas compte. Cela empêchera plusieurs threads d’accéder aux compteurs à la fois, mais les valeurs de ces compteurs sont toujours susceptibles de changer même lorsque le mutex est en vigueur.

Donc, vous marquez les vars comme volatiles, car ils peuvent être modifiés de l’extérieur, et non pas parce qu’ils sont à l’intérieur d’une garde mutex.

Gardez-les volatiles.

Bien que cela dépende de la bibliothèque de thread que vous utilisez, ma compréhension est que toute bibliothèque décente ne nécessitera pas l’ utilisation de volatile .

Dans Pthreads, par exemple , l’utilisation d’un mutex garantit que vos données sont correctement mémorisées.

EDIT: Par la présente, j’approuve la réponse de Tony comme étant meilleure que la mienne.

Vous avez toujours besoin du mot clé “volatile”.

Les mutex empêchent les compteurs d’accéder simultanément.

“volatile” indique au compilateur d’utiliser réellement le compteur au lieu de le mettre en cache dans un registre de CPU (qui ne serait pas mis à jour par le thread simultané).