web-dev-qa-db-fra.com

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

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

279
Doron Yaacoby

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

En C #, "volatile" signifie non seulement "assurez-vous que le compilateur et la gigue N'effectuent aucune réorganisation de code ni aucune mise en cache des registres Optimisations sur cette variable". Cela signifie également "dire aux processeurs de Faire tout ce dont ils ont besoin pour s'assurer que je lis la dernière valeur , Même si cela signifie arrêter d'autres processeurs et les faire synchroniser avec leurs mémoires principale." caches ".

En fait, ce dernier morceau est un mensonge. La vraie sémantique des lectures volatiles et les écritures sont considérablement plus complexes que ce que j'ai décrit ici; dans fact ils ne garantissent pas réellement que chaque processeur arrête ce qu'il est en train de faire.} et met à jour les caches de/vers la mémoire principale. Au lieu de cela, {ils fournissent Des garanties plus faibles sur la façon dont la mémoire accède avant et après les lectures et Les écritures peuvent être observées pour être ordonnées les unes par rapport aux autres} _. Certaines opérations telles que la création d'un nouveau thread, la saisie d'un verrou ou en utilisant l’une des méthodes de la famille Interlocked, introduisez plus fort garanties sur l'observation de la commande. Si vous voulez plus de détails, lisez les sections 3.10 et 10.5.3 de la spécification C # 4.0.

Franchement, je vous décourage de faire un champ instable}. Volatil les champs sont un signe que vous faites quelque chose de complètement fou: vous êtes essayer de lire et d’écrire la même valeur sur deux threads différents sans mettre un verrou en place. Les serrures garantissent que la mémoire est lue ou les modifications apportées à l’intérieur de la serrure sont cohérentes, la garantie des serrures qu'un seul thread accède à un bloc de mémoire donné à la fois, et ainsi sur. Le nombre de situations dans lesquelles un verrou est trop lent est très petit, 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 grand. JE n'essayez pas d'écrire un code de bas niveau de verrouillage, à l'exception du plus trivial usages des opérations interverrouillées. Je laisse l'usage de "volatile" à de vrais experts.

Pour en savoir plus, voir:

254
Ohad Schneider

Si vous voulez obtenir un peu plus de détails techniques sur le mot clé volatile, considérez 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;
}

À l'aide des paramètres standard du compilateur optimisés (édition), 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 attribué i au registre eax. Assez simple. Il existe cependant quelques bits intéressants: l'instruction lea ebx, [ebx] est en réalité 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 boucle au lieu d'utiliser une instruction inc eax. L'add add reg, reg a une latence inférieure sur quelques cœurs IA32 par rapport à l'instruction inc reg, mais n'a jamais une latence supérieure. 

Passons maintenant à la boucle avec le compteur de boucles volatiles. 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 affectée à un registre. Le compilateur va même jusqu'à ne pas effectuer un chargement/une incrémentation/un stockage en trois étapes distinctes (load eax, inc eax, save eax) lors de la mise à jour de la valeur du compteur. La mémoire est alors directement modifiée dans une seule instruction (un add mem , reg). La manière dont le code a été créé garantit que la valeur du compteur de boucle est toujours à jour dans le contexte d'un seul cœur de processeur. Aucune opération sur les données ne peut entraîner une corruption ou une perte de données (par conséquent, n'utilisez pas le chargement/inc/magasin car la valeur peut changer au cours de l'inc, et donc être perdue dans le magasin). Les interruptions ne pouvant être traitées qu'une fois l'instruction en cours terminée, les données ne peuvent jamais être corrompues, même avec une mémoire non alignée.

Une fois que vous avez introduit une seconde CPU dans le système, le mot-clé volatile ne protège pas contre la mise à jour simultanée des données mises à jour par une autre CPU. Dans l'exemple ci-dessus, vous auriez besoin que les données ne soient pas alignées pour être corrompues. Le mot clé volatile n'empêchera pas la corruption potentielle si les données ne peuvent pas être traitées de manière atomique, par exemple, si le compteur de boucle était de type long long (64 bits), il faudrait alors deux opérations 32 bits pour mettre à jour la valeur, au milieu de qui une interruption peut se produire et changer les données.

Ainsi, le mot clé volatile n'est utile que pour des données alignées 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 les opérations IO où IO était en constante évolution, mais avait une adresse constante, telle qu'un périphérique mappé en mémoire UART, et le compilateur ne devrait pas continue à réutiliser la première valeur lue à partir de l'adresse.

Si vous gérez des données volumineuses ou si vous 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.

54
Skizz

Si vous utilisez .NET 1.1, le mot clé volatile est nécessaire lors du verrouillage vérifié. Pourquoi? Dans les situations antérieures à .NET 2.0, le scénario suivant pouvait faire en sorte qu'un deuxième thread accède à un objet non-null, mais pas complètement construit:

  1. Le fil 1 demande si une variable est null . // if (this.foo == null)
  2. Le thread 1 détermine que la variable est null, insère donc un verrou . // lock (this.bar)
  3. Le fil 1 demande ENCORE si la variable est null . // if (this.foo == null)
  4. Le thread 1 détermine toujours que la variable est null, il appelle donc un constructeur et affecte la valeur à la variable . // this.foo = new Foo ();

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

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

Avant .NET 2.0, vous pouviez déclarer this.foo comme étant volatile pour résoudre ce problème. Depuis .NET 2.0, il n’est plus nécessaire d’utiliser le mot clé volatile pour effectuer un double contrôle du verrouillage.

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

38
AndrewTek

De MSDN : Le modificateur volatile est généralement utilisé pour un champ auquel plusieurs threads ont accès sans utiliser l'instruction lock 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.

22
Dr. Bob

Parfois, le compilateur optimise un champ et utilise un registre pour le stocker. Si le thread 1 écrit dans le champ et qu'un autre thread y accède, étant donné que la mise à jour était stockée dans un registre (et non en mémoire), le second thread obtiendrait des données périmées.

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

20
Benoit

Le CLR aime optimiser les instructions. Ainsi, 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 fait de marquer un champ comme étant volatile garantit 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 par un autre code s'exécutant dans le système d'exploitation.

Vous perdez évidemment un peu d'optimisation, mais le code reste plus simple.

13
Joseph Daigle

Le compilateur modifie parfois l'ordre des instructions dans le code pour l'optimiser. Normalement, cela ne pose pas de problème dans un environnement mono-thread, mais peut-être dans un environnement multi-thread. 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 pas à une sortie ou à "Valeur: 10" comme résultat. Il se peut que le compilateur change de ligne dans la fonction t1. Si t2 s'exécute alors, il se peut que _flag ait la valeur 5, mais que _value ait 0. La logique attendue pourrait donc être interrompue. 

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

private static volatile int _flag = 0;

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

0
Aliaksei Maniuk

Donc, pour résumer tout cela, la réponse correcte à la question est: Si votre code est exécuté dans la version 2.0 ou ultérieure, le mot clé volatile n'est presque jamais nécessaire et fait plus de tort que de bien s'il est utilisé inutilement. C'EST À DIRE. Ne l'utilisez jamais. MAIS dans les versions antérieures du moteur d’exécution, il IS était nécessaire pour un double contrôle correct du verrouillage des champs statiques. Spécifiquement les champs statiques dont la classe a un code d'initialisation de classe statique.

0
Paul Easter