Quand faut-il utiliser le mot-clé volatile en C #?

299

Quelqu'un peut-il fournir une bonne explication du mot clé volatile en C #? Quels problèmes résout-il et lequel ne résout pas? Dans quels cas cela me sauvera-t-il de l'utilisation du verrouillage?

Doron Yaacoby
la source
6
Pourquoi voulez-vous économiser sur l'utilisation du verrouillage? Les verrous non intentionnels ajoutent quelques nanosecondes à votre programme. Ne pouvez-vous vraiment pas vous permettre quelques nanosecondes?
Eric Lippert

Réponses:

273

Je ne pense pas qu'il y ait une meilleure personne pour répondre à cela qu'Eric Lippert (souligné dans l'original):

En C #, "volatile" signifie non seulement "assurez-vous que le compilateur et la gigue n'effectuent pas de réorganisation du code ou n'enregistrent pas les optimisations de mise en cache sur cette variable". Cela signifie également "dire aux processeurs de faire tout ce qu'ils doivent faire pour s'assurer que je lis la dernière valeur, même si cela signifie arrêter d'autres processeurs et les obliger à synchroniser la mémoire principale avec leurs caches".

En fait, ce dernier morceau est un mensonge. La véritable sémantique des lectures et des écritures volatiles est considérablement plus complexe que celle que j'ai décrite ici; en fait, ils ne garantissent pas réellement que chaque processeur arrête ce qu'il fait et met à jour les caches vers / depuis la mémoire principale. Au contraire, ils fournissent des garanties plus faibles sur la façon dont les accès en mémoire avant et après les lectures et les écritures peuvent être observés pour être ordonnés les uns par rapport aux autres . Certaines opérations telles que la création d'un nouveau thread, l'entrée d'un verrou ou l'utilisation de l'une des méthodes de la famille Interlocked introduisent des garanties plus solides quant à l'observation de la commande. Si vous souhaitez plus de détails, lisez les sections 3.10 et 10.5.3 de la spécification C # 4.0.

Franchement, je vous déconseille de créer un champ volatile . Les champs volatils sont un signe que vous faites quelque chose de complètement fou: vous essayez de lire et d'écrire la même valeur sur deux threads différents sans mettre de verrou en place. Les verrous garantissent que la mémoire lue ou modifiée à l'intérieur du verrou est respectée, les verrous garantissent qu'un seul thread accède à un bloc de mémoire donné à la fois, et ainsi de suite. Le nombre de situations dans lesquelles un verrou est trop lent est très faible et la probabilité que vous vous trompiez de code parce que vous ne comprenez pas le modèle de mémoire exact est très grande. Je n'essaie pas d'écrire de code à faible verrouillage, sauf pour les utilisations les plus triviales des opérations interverrouillées. Je laisse l'usage de «volatile» à de vrais experts.

Pour plus d'informations, voir:

Ohad Schneider
la source
29
Je voterais contre si je le pouvais. Il y a beaucoup d'informations intéressantes là-dedans, mais cela ne répond pas vraiment à sa question. Il pose des questions sur l'utilisation du mot-clé volatile en ce qui concerne le verrouillage. Pendant un certain temps (avant 2.0 RT), le mot-clé volatile devait être utilisé pour sécuriser correctement un thread de champ statique si l'instance de champ avait un code d'initialisation dans le constructeur (voir la réponse d'AndrewTek). Il y a encore beaucoup de code 1.1 RT dans les environnements de production et les développeurs qui le maintiennent devraient savoir pourquoi ce mot-clé est là et s'il est sûr de le supprimer.
Paul Easter
3
@PaulEaster le fait qu'il puisse être utilisé pour un verrouillage vérifié par doulbe (généralement dans le modèle singleton) ne signifie pas qu'il devrait . S'appuyer sur le modèle de mémoire .NET est probablement une mauvaise pratique - vous devriez plutôt vous fier au modèle ECMA. Par exemple, vous souhaiterez peut-être effectuer un port en mono un jour, qui peut avoir un modèle différent. Je dois également comprendre que différentes architectures matérielles pourraient changer les choses. Pour plus d'informations, voir: stackoverflow.com/a/7230679/67824 . Pour de meilleures alternatives singleton (pour toutes les versions .NET), voir: csharpindepth.com/articles/general/singleton.aspx
Ohad Schneider
6
En d'autres termes, la bonne réponse à la question est: si votre code s'exécute dans le runtime 2.0 ou ultérieur, le mot clé volatile n'est presque jamais nécessaire et fait plus de mal que de bien s'il est utilisé inutilement. Mais dans les versions antérieures du runtime, il EST nécessaire pour un verrouillage correct de la double vérification sur les champs statiques.
Paul Easter
3
cela signifie-t-il que les verrous et les variables volatiles s'excluent mutuellement dans le sens suivant: si j'ai utilisé des verrous autour d'une variable, il n'est plus nécessaire de déclarer cette variable comme volatile?
giorgim
4
@Giorgi yes - les barrières de mémoire garanties par volatileseront là grâce au verrou
Ohad Schneider
54

Si vous voulez obtenir un peu plus de détails techniques sur ce que fait le mot-clé volatile, envisagez le programme suivant (j'utilise DevStudio 2005):

#include <iostream>
void main()
{
  int j = 0;
  for (int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  std::cout << j;
}

En utilisant les paramètres du compilateur optimisé (version) standard, le compilateur crée l'assembleur suivant (IA32):

void main()
{
00401000  push        ecx  
  int j = 0;
00401001  xor         ecx,ecx 
  for (int i = 0 ; i < 100 ; ++i)
00401003  xor         eax,eax 
00401005  mov         edx,1 
0040100A  lea         ebx,[ebx] 
  {
    j += i;
00401010  add         ecx,eax 
00401012  add         eax,edx 
00401014  cmp         eax,64h 
00401017  jl          main+10h (401010h) 
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
00401019  mov         dword ptr [esp],0 
00401020  mov         eax,dword ptr [esp] 
00401023  cmp         eax,64h 
00401026  jge         main+3Eh (40103Eh) 
00401028  jmp         main+30h (401030h) 
0040102A  lea         ebx,[ebx] 
  {
    j += i;
00401030  add         ecx,dword ptr [esp] 
00401033  add         dword ptr [esp],edx 
00401036  mov         eax,dword ptr [esp] 
00401039  cmp         eax,64h 
0040103C  jl          main+30h (401030h) 
  }
  std::cout << j;
0040103E  push        ecx  
0040103F  mov         ecx,dword ptr [__imp_std::cout (40203Ch)] 
00401045  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)] 
}
0040104B  xor         eax,eax 
0040104D  pop         ecx  
0040104E  ret              

En regardant la sortie, le compilateur a décidé d'utiliser le registre ecx pour stocker la valeur de la variable j. Pour la boucle non volatile (la première), le compilateur a affecté i au registre eax. Assez simple. Il y a cependant quelques bits intéressants - l'instruction lea ebx, [ebx] est en fait une instruction nop à plusieurs octets, de sorte que la boucle passe à une adresse mémoire alignée sur 16 octets. L'autre est l'utilisation d'edx pour incrémenter le compteur de boucles au lieu d'utiliser une instruction inc eax. L'instruction add reg, reg a une latence plus faible sur quelques cœurs IA32 par rapport à l'instruction inc inc, mais n'a jamais une latence plus élevée.

Passons maintenant à la boucle avec le compteur de boucle volatile. Le compteur est stocké dans [esp] et le mot-clé volatile indique au compilateur que la valeur doit toujours être lue / écrite dans la mémoire et jamais assignée à un registre. Le compilateur va même jusqu'à ne pas charger / incrémenter / stocker en trois étapes distinctes (charger eax, inc eax, save eax) lors de la mise à jour de la valeur du compteur, au lieu de cela la mémoire est directement modifiée dans une seule instruction (un add mem , reg). La façon dont le code a été créé garantit que la valeur du compteur de boucles est toujours à jour dans le contexte d'un seul cœur de processeur. Aucune opération sur les données ne peut entraîner de corruption ou de perte de données (donc ne pas utiliser la charge / inc / store car la valeur peut changer pendant l'inc et donc être perdue sur le magasin). Étant donné que les interruptions ne peuvent être réparées qu'une fois l'instruction en cours terminée,

Une fois que vous avez introduit un deuxième processeur dans le système, le mot-clé volatile ne protège pas contre la mise à jour des données par un autre processeur en même temps. Dans l'exemple ci-dessus, vous auriez besoin que les données ne soient pas alignées pour obtenir une corruption potentielle. Le mot-clé volatile n'empêchera pas une corruption potentielle si les données ne peuvent pas être gérées de manière atomique, par exemple, si le compteur de boucle était de type long long (64 bits), alors il faudrait deux opérations 32 bits pour mettre à jour la valeur, au milieu de une interruption peut survenir et modifier les données.

Ainsi, le mot-clé volatile n'est bon que pour les données alignées qui sont inférieures ou égales à la taille des registres natifs de sorte que les opérations sont toujours atomiques.

Le mot-clé volatile a été conçu pour être utilisé avec des opérations d'E / S où l'E / S serait en constante évolution, mais avait une adresse constante, comme un périphérique UART mappé en mémoire, et le compilateur ne devrait pas continuer à réutiliser la première valeur lue à partir de l'adresse.

Si vous manipulez des données volumineuses ou avez plusieurs processeurs, vous aurez besoin d'un système de verrouillage de niveau supérieur (OS) pour gérer correctement l'accès aux données.

Skizz
la source
C'est C ++ mais le principe s'applique à C #.
Skizz
6
Eric Lippert écrit que volatile en C ++ empêche seulement le compilateur d'effectuer certaines optimisations, tandis qu'en C # volatile fait en outre une communication entre les autres cœurs / processeurs pour s'assurer que la dernière valeur est lue.
Peter Huber du
44

Si vous utilisez .NET 1.1, le mot clé volatile est nécessaire lorsque vous effectuez un verrouillage à double vérification. Pourquoi? Parce qu'avant .NET 2.0, le scénario suivant pouvait entraîner un deuxième thread à accéder à un objet non null, mais pas entièrement construit:

  1. Le thread 1 demande si une variable est nulle. //if(this.foo == null)
  2. Le thread 1 détermine que la variable est nulle, donc entre dans un verrou. //lock(this.bar)
  3. Le thread 1 demande à nouveau si la variable est nulle. //if(this.foo == null)
  4. Le thread 1 détermine toujours que la variable est nulle, il appelle donc un constructeur et attribue la valeur à la variable. //this.foo = new Foo ();

Avant .NET 2.0, this.foo pouvait se voir attribuer la nouvelle instance de Foo, avant la fin de l'exécution du constructeur. Dans ce cas, un deuxième thread pourrait entrer (lors de l'appel du thread 1 au constructeur de Foo) et rencontrer les problèmes suivants:

  1. Le thread 2 demande si la variable est nulle. //if(this.foo == null)
  2. Le thread 2 détermine que la variable n'est PAS nulle, donc essaie de l'utiliser. //this.foo.MakeFoo ()

Avant .NET 2.0, vous pouviez déclarer this.foo comme volatile pour contourner ce problème. Depuis .NET 2.0, vous n'avez plus besoin d'utiliser le mot clé volatile pour effectuer un verrouillage à double vérification.

Wikipedia a en fait un bon article sur le verrouillage à double vérification, et aborde brièvement ce sujet: http://en.wikipedia.org/wiki/Double-checked_locking

AndrewTek
la source
2
c'est exactement ce que je vois dans un code hérité et je me posais des questions à ce sujet. c'est pourquoi j'ai commencé une recherche plus approfondie. Merci!
Peter Porfy
1
Je ne comprends pas comment le thread 2 attribuerait-il de la valeur foo? Le thread 1 ne se verrouille-t-il pas this.baret donc seul le thread 1 pourra initialiser foo à un moment donné? Je veux dire, vous vérifiez la valeur après le
déverrouillage à
25

Parfois, le compilateur optimise un champ et utilise un registre pour le stocker. Si le thread 1 fait une écriture dans le champ et qu'un autre thread y accède, puisque la mise à jour a été stockée dans un registre (et non en mémoire), le 2ème thread obtiendrait des données périmées.

Vous pouvez penser au mot clé volatile comme disant au compilateur "Je veux que vous stockiez cette valeur en mémoire". Cela garantit que le 2ème thread récupère la dernière valeur.

Benoit
la source
21

À partir de MSDN : le modificateur volatile est généralement utilisé pour un champ auquel plusieurs threads accèdent sans utiliser l'instruction de verrouillage pour sérialiser l'accès. L'utilisation du modificateur volatile garantit qu'un thread récupère la valeur la plus récente écrite par un autre thread.

Dr. Bob
la source
13

Le CLR aime optimiser les instructions, donc lorsque vous accédez à un champ en code, il peut ne pas toujours accéder à la valeur actuelle du champ (cela peut provenir de la pile, etc.). Le marquage d'un champ volatilegarantit que la valeur actuelle du champ est accessible par l'instruction. Cela est utile lorsque la valeur peut être modifiée (dans un scénario non verrouillable) par un thread simultané dans votre programme ou un autre code exécuté dans le système d'exploitation.

Vous perdez évidemment une certaine optimisation, mais cela rend le code plus simple.

Joseph Daigle
la source
3

J'ai trouvé cet article de Joydip Kanjilal très utile!

When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value

Je vais juste le laisser ici pour référence

Christina Katsakoula
la source
0

Le compilateur modifie parfois l'ordre des instructions dans le code pour l'optimiser. Normalement, ce n'est pas un problème dans un environnement à un seul thread, mais cela peut être un problème dans un environnement à plusieurs threads. Voir l'exemple suivant:

 private static int _flag = 0;
 private static int _value = 0;

 var t1 = Task.Run(() =>
 {
     _value = 10; /* compiler could switch these lines */
     _flag = 5;
 });

 var t2 = Task.Run(() =>
 {
     if (_flag == 5)
     {
         Console.WriteLine("Value: {0}", _value);
     }
 });

Si vous exécutez t1 et t2, vous ne vous attendez à aucune sortie ni à "Valeur: 10" comme résultat. Il se peut que le compilateur commute la ligne à l'intérieur de la fonction t1. Si t2 s'exécute alors, il se pourrait que _flag ait une valeur de 5, mais que _value ait 0. La logique attendue pourrait donc être brisée.

Pour résoudre ce problème, vous pouvez utiliser un mot clé volatile que vous pouvez appliquer au champ. Cette instruction désactive les optimisations du compilateur afin que vous puissiez forcer l'ordre correct dans votre code.

private static volatile int _flag = 0;

Vous ne devez utiliser volatile que si vous en avez vraiment besoin, car il désactive certaines optimisations du compilateur, il nuira aux performances. Il n'est pas non plus pris en charge par tous les langages .NET (Visual Basic ne le prend pas en charge), ce qui entrave l'interopérabilité des langues.

Aliaksei Maniuk
la source
2
Votre exemple est vraiment mauvais. Le programmeur ne devrait jamais avoir aucune attente sur la valeur de _flag dans la tâche t2 basée sur le fait que le code de t1 est écrit en premier. Écrit en premier! = Exécuté en premier. Peu importe que le compilateur permute ces deux lignes en t1. Même si le compilateur n'a pas changé ces instructions, votre Console.WriteLne dans la branche else peut toujours s'exécuter, même AVEC le mot clé volatile sur _flag.
Jakotheshadows
@jakotheshadows, vous avez raison, j'ai modifié ma réponse. Mon idée principale était de montrer que la logique attendue pouvait être brisée lorsque nous
exécutons
0

Donc, pour résumer tout cela, la bonne réponse à la question est: si votre code s'exécute dans l'exécution 2.0 ou ultérieure, le mot clé volatile n'est presque jamais nécessaire et fait plus de mal que de bien s'il est utilisé inutilement. IE Ne l'utilisez jamais. MAIS dans les versions antérieures du runtime, il EST nécessaire pour un verrouillage correct de la double vérification sur les champs statiques. Champs spécifiquement statiques dont la classe a un code d'initialisation de classe statique.

Paul Pâques
la source
-4

plusieurs threads peuvent accéder à une variable. La dernière mise à jour concernera la variable

user2943601
la source