Question

J'ai une classe de verrouillage 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 "volatile". mot-clé est une indication de la durée de la compilation au compilateur pour faire la bonne chose tout en faisant les optimisations.

Cordialement, -Jay.

Était-ce utile?

La solution

Il y a 2 éléments fondamentalement sans lien ici, qui sont toujours confondus.

  • volatile
  • fils, verrous, barrières de mémoire, etc.

volatile est utilisé pour indiquer 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.), citées de Herb Sutter dans une autre réponse, servent à empêcher le CPU de réorganiser les demandes de mémoire en lecture / écriture, quelle que soit la façon dont le compilateur a dit pour le faire. ie 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 verrouillage, la raison pour laquelle volatile n'est PAS nécessaire, est due au fait que des appels de fonction ont été effectués dans un souci de verrouillage. 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 relit x lors de son 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 n'est pas certain que lock () modifie obj. Vous pouvez probablement configurer les indicateurs de compilation pour optimiser de manière agressive (aucun 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 passer de temp2 à temp3, vous utiliseriez volatile (et votre verrouillage 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 une des deux choses ici:   - le code en ligne appelle éventuellement une fonction de verrouillage 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 intégrées telles que __InterlockedCompareExchange) que votre compilateur reconnaîtra et évitera ainsi de réorganiser.

EDIT: P.S. J'ai oublié de mentionner - pour les projets pthreads, certains compilateurs sont marqués comme "conformes à POSIX". ce qui signifie, entre autres choses, qu'ils reconnaîtront les fonctions de pthread_ et ne feront pas de mauvaises optimisations autour d'eux. 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.

Autres conseils

Extrait de l'article de Herb Sutter "Utiliser les 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étrique de clôture à sens unique pour le début et la fin de toute section critique, illustrée par les flèches dans 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 une exécution ultérieure.
  •   
  • 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 que le code ne peut pas franchir la clôture vers le bas, mais uniquement vers le haut. Cela garantit que tout autre fil qui verra la version finale de l'écriture verra également toutes les écritures qui la précèdent.
  •   

Ainsi, pour qu'un compilateur produise le code correct pour une plate-forme cible, lorsqu'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 d'une chose protégée par un code CRITICAL_SECTION - la section critique peut être protégée par d'autres objets de synchronisation) la sémantique correcte d'acquisition et de publication doit être suivie. Vous ne devriez donc pas avoir à marquer les variables partagées comme volatiles tant qu'elles ne sont accessibles que dans des 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 le charger dans un registre et de supposer que cela 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 protection mutex.

Maintenez-les volatiles.

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

Dans Pthreads, par exemple

EDIT: , j'approuve La réponse de Tony est meilleure que la mienne.

Vous avez toujours besoin de l'option "volatile". mot clé.

Les mutex empêchent les compteurs d’accéder aux accès concurrents.

"volatile" indique au compilateur d'utiliser réellement le compteur au lieu de le mettre en cache dans un registre de la CPU (ce qui ne être mis à jour par le thread simultané).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top