Quando utilizzare la parola chiave volatile in C #?


299

Qualcuno può fornire una buona spiegazione della parola chiave volatile in C #? Quali problemi risolve e quali no? In quali casi mi risparmierà l'uso del blocco?


6
Perché vuoi risparmiare sull'uso del blocco? I blocchi non aggiunti aggiungono qualche nanosecondo al programma. Non puoi davvero permetterti qualche nanosecondo?
Eric Lippert,

Risposte:


273

Non credo che ci sia una persona migliore a cui rispondere rispetto a Eric Lippert (enfasi nell'originale):

In C #, "volatile" significa non solo "assicurarsi che il compilatore e il jitter non eseguano alcun riordino del codice o registri le ottimizzazioni della cache su questa variabile". Significa anche "dire ai processori di fare tutto ciò che è necessario per assicurarsi che io stia leggendo l'ultimo valore, anche se ciò significa arrestare altri processori e farli sincronizzare la memoria principale con le loro cache".

In realtà, quell'ultimo pezzo è una bugia. La vera semantica delle letture e delle scritture volatili è considerevolmente più complessa di quanto ho delineato qui; in realtà non garantiscono che ogni processore arresti ciò che sta facendo e aggiorni le cache alla / dalla memoria principale. Piuttosto, forniscono garanzie più deboli su come gli accessi alla memoria prima e dopo le letture e le scritture possano essere osservate l'una rispetto all'altra . Alcune operazioni come la creazione di un nuovo thread, l'inserimento di un lucchetto o l'utilizzo di uno dei metodi della famiglia Interlocked introducono maggiori garanzie sull'osservazione dell'ordine. Se vuoi maggiori dettagli, leggi le sezioni 3.10 e 10.5.3 della specifica C # 4.0.

Francamente, ti scoraggio di non creare mai un campo instabile . I campi volatili indicano che stai facendo qualcosa di assolutamente folle: stai tentando di leggere e scrivere lo stesso valore su due thread diversi senza mettere un lucchetto in posizione. I blocchi garantiscono che la memoria letta o modificata all'interno del blocco sia osservata come coerente, i blocchi garantiscono che solo un thread acceda a un determinato blocco di memoria alla volta e così via. Il numero di situazioni in cui un blocco è troppo lento è molto piccolo e la probabilità che tu sbagli il codice perché non capisci il modello esatto di memoria è molto grande. Non tento di scrivere alcun codice di blocco basso, tranne per gli usi più banali delle operazioni interbloccate. Lascio l'uso del "volatile" a veri esperti.

Per ulteriori letture vedi:


29
Voterei questo se potessi. Ci sono molte informazioni interessanti lì dentro, ma in realtà non risponde alla sua domanda. Sta chiedendo informazioni sull'uso della parola chiave volatile in relazione al blocco. Per un po 'di tempo (prima di 2.0 RT), la parola chiave volatile era necessaria da usare per rendere correttamente sicuro un thread di campo statico se l'istanza di campo aveva un codice di inizializzazione nel costruttore (vedere la risposta di AndrewTek). C'è ancora molto codice RT 1.1 negli ambienti di produzione e gli sviluppatori che lo mantengono dovrebbero sapere perché quella parola chiave è presente e se è sicura da rimuovere.
Paul Easter

3
@PaulEaster il fatto che possa essere utilizzato per il blocco controllato da Doulbe (di solito nel modello singleton) non significa che dovrebbe . Affidarsi al modello di memoria .NET è probabilmente una cattiva pratica: si dovrebbe invece fare affidamento sul modello ECMA. Ad esempio, potresti voler eseguire il port in mono un giorno, che potrebbe avere un modello diverso. Devo anche capire che diverse architetture hardware potrebbero cambiare le cose. Per ulteriori informazioni, consultare: stackoverflow.com/a/7230679/67824 . Per migliori alternative a singleton (per tutte le versioni .NET) consultare: csharpindepth.com/articles/general/singleton.aspx
Ohad Schneider

6
In altre parole, la risposta corretta alla domanda è: se il tuo codice è in esecuzione nel runtime 2.0 o successivo, la parola chiave volatile non è quasi mai necessaria e fa più male che bene se usata inutilmente. Ma nelle versioni precedenti del runtime, è necessario per un corretto doppio controllo sui campi statici.
Paul Easter,

3
questo significa che i blocchi e le variabili volatili si escludono a vicenda nel seguente senso: se ho usato i blocchi attorno a qualche variabile non è più necessario dichiarare quella variabile come volatile?
giorgim,

4
@Giorgi sì - le barriere di memoria garantite volatilesaranno lì in virtù della chiusura
Ohad Schneider

54

Se vuoi ottenere un po 'più tecnico su ciò che fa la parola chiave volatile, considera il seguente programma (sto usando 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;
}

Utilizzando le impostazioni standard del compilatore ottimizzato (rilascio), il compilatore crea il seguente assemblatore (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              

Osservando l'output, il compilatore ha deciso di utilizzare il registro ecx per memorizzare il valore della variabile j. Per il loop non volatile (il primo) il compilatore ha assegnato i al registro eax. Abbastanza semplice. Tuttavia, ci sono un paio di bit interessanti: l'istruzione lea ebx, [ebx] è effettivamente un'istruzione nop multibyte in modo che il loop salti a un indirizzo di memoria allineato a 16 byte. L'altro è l'uso di edx per incrementare il contatore di loop invece di usare un'istruzione inc eax. L'istruzione add reg, reg ha una latenza inferiore su alcuni core IA32 rispetto all'istruzione inc reg, ma non ha mai una latenza più alta.

Ora per il loop con il contatore loop volatile. Il contatore è memorizzato in [esp] e la parola chiave volatile indica al compilatore che il valore deve sempre essere letto / scritto in memoria e mai assegnato a un registro. Il compilatore si spinge fino al punto di non eseguire un caricamento / incremento / memorizzazione in tre passaggi distinti (caricamento di eax, inc eax, salvataggio di eax) quando si aggiorna il valore del contatore, invece la memoria viene modificata direttamente in una singola istruzione (un mem aggiuntivo , reg). Il modo in cui il codice è stato creato garantisce che il valore del contatore di loop sia sempre aggiornato nel contesto di un singolo core della CPU. Nessuna operazione sui dati può comportare il danneggiamento o la perdita di dati (quindi non utilizzare il carico / inc / negozio poiché il valore può cambiare durante l'inc, perdendo così nel negozio). Poiché gli interrupt possono essere sottoposti a manutenzione solo una volta completate le istruzioni correnti,

Una volta introdotta una seconda CPU nel sistema, la parola chiave volatile non protegge dai dati che vengono aggiornati da un'altra CPU contemporaneamente. Nell'esempio sopra, per ottenere una potenziale corruzione è necessario che i dati non siano allineati. La parola chiave volatile non impedirà il potenziale danneggiamento se i dati non possono essere gestiti atomicamente, ad esempio, se il contatore di loop era di tipo long long (64 bit), occorrerebbero due operazioni a 32 bit per aggiornare il valore, nel mezzo di quale interruzione può verificarsi e modificare i dati.

Pertanto, la parola chiave volatile è valida solo per i dati allineati che sono inferiori o uguali alla dimensione dei registri nativi in ​​modo tale che le operazioni siano sempre atomiche.

La parola chiave volatile è stata concepita per essere utilizzata con operazioni di I / O in cui l'IO sarebbe in costante cambiamento ma con un indirizzo costante, come un dispositivo UART mappato in memoria, e il compilatore non dovrebbe continuare a riutilizzare il primo valore letto dall'indirizzo.

Se stai gestendo dati di grandi dimensioni o hai più CPU, avrai bisogno di un sistema di blocco di livello superiore (OS) per gestire correttamente l'accesso ai dati.


Questo è C ++ ma il principio si applica a C #.
Skizz,

6
Eric Lippert scrive che la volatilità in C ++ impedisce solo al compilatore di eseguire alcune ottimizzazioni, mentre in C # volatile inoltre effettua alcune comunicazioni tra gli altri core / processori per garantire che venga letto l'ultimo valore.
Peter Huber,

44

Se si utilizza .NET 1.1, la parola chiave volatile è necessaria quando si esegue il blocco con doppio controllo. Perché? Perché prima di .NET 2.0, il seguente scenario poteva causare un secondo thread per accedere a un oggetto non nullo, ma non completamente costruito:

  1. La discussione 1 chiede se una variabile è nulla. //if(this.foo == null)
  2. Il thread 1 determina che la variabile è nulla, quindi inserisce un blocco. //lock(this.bar)
  3. Il thread 1 chiede DI NUOVO se la variabile è nulla. //if(this.foo == null)
  4. Il thread 1 determina ancora che la variabile è nulla, quindi chiama un costruttore e assegna il valore alla variabile. //this.foo = new Foo ();

Prima di .NET 2.0, questo.foo poteva essere assegnato alla nuova istanza di Foo, prima che il costruttore avesse terminato l'esecuzione. In questo caso, potrebbe venire un secondo thread (durante la chiamata del thread 1 al costruttore di Foo) e provare quanto segue:

  1. Il thread 2 chiede se la variabile è nulla. //if(this.foo == null)
  2. Il thread 2 determina che la variabile NON è nulla, quindi cerca di usarla. //this.foo.MakeFoo ()

Prima di .NET 2.0, è possibile dichiarare this.foo come volatile per aggirare questo problema. A partire da .NET 2.0, non è più necessario utilizzare la parola chiave volatile per ottenere un doppio blocco verificato.

Wikipedia in realtà ha un buon articolo su Double Checked Locking e tocca brevemente questo argomento: http://en.wikipedia.org/wiki/Double-checked_locking


2
questo è esattamente quello che vedo in un codice legacy e me lo chiedevo. ecco perché ho iniziato una ricerca più approfondita. Grazie!
Peter Porfy,

1
Non capisco come assegnerebbe il valore al thread 2 foo? Il thread 1 non si blocca this.bare quindi solo il thread 1 sarà in grado di inizializzare foo in un dato momento? Voglio dire, controlli il valore dopo il rilascio del blocco, quando comunque dovrebbe avere il nuovo valore dal thread 1.
Gilmishal

24

A volte, il compilatore ottimizzerà un campo e utilizzerà un registro per memorizzarlo. Se il thread 1 esegue una scrittura sul campo e un altro thread vi accede, poiché l'aggiornamento è stato memorizzato in un registro (e non nella memoria), il secondo thread otterrebbe dati non aggiornati.

Puoi pensare alla parola chiave volatile come dire al compilatore "Voglio che tu memorizzi questo valore in memoria". Ciò garantisce che il secondo thread recuperi l'ultimo valore.


21

Da MSDN : il modificatore volatile viene solitamente utilizzato per un campo a cui accedono più thread senza utilizzare l'istruzione lock per serializzare l'accesso. L'uso del modificatore volatile assicura che un thread recuperi il valore più aggiornato scritto da un altro thread.


13

Al CLR piace ottimizzare le istruzioni, quindi quando si accede a un campo nel codice potrebbe non sempre accedere al valore corrente del campo (potrebbe essere dallo stack, ecc.). Contrassegnare un campo in modo volatileche l'istruzione acceda al valore corrente del campo. Ciò è utile quando il valore può essere modificato (in uno scenario non bloccante) da un thread simultaneo nel programma o da qualche altro codice in esecuzione nel sistema operativo.

Ovviamente perdi un po 'di ottimizzazione, ma mantiene il codice più semplice.


3

Ho trovato questo articolo di Joydip Kanjilal molto 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

Lascio qui per riferimento


0

Il compilatore a volte modifica l'ordine delle istruzioni nel codice per ottimizzarlo. Normalmente questo non è un problema nell'ambiente a thread singolo, ma potrebbe essere un problema nell'ambiente a thread multipli. Vedi il seguente esempio:

 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);
     }
 });

Se esegui t1 e t2, non ti aspetteresti alcun risultato o "Valore: 10" come risultato. È possibile che il compilatore commuti la riga all'interno della funzione t1. Se t2 viene quindi eseguito, potrebbe essere che _flag abbia un valore di 5, ma _value abbia 0. Quindi la logica prevista potrebbe essere rotta.

Per risolvere questo problema puoi usare una parola chiave volatile che puoi applicare al campo. Questa affermazione disabilita le ottimizzazioni del compilatore in modo da poter forzare l'ordine corretto nel tuo codice.

private static volatile int _flag = 0;

Dovresti usare volatile solo se ne hai davvero bisogno, perché disabilita alcune ottimizzazioni del compilatore, danneggerà le prestazioni. Inoltre, non è supportato da tutti i linguaggi .NET (Visual Basic non lo supporta), quindi ostacola l'interoperabilità del linguaggio.


2
Il tuo esempio è davvero pessimo. Il programmatore non dovrebbe mai avere aspettative sul valore di _flag nell'attività t2 in base al fatto che il codice di t1 viene scritto per primo. Scritto per primo! = Eseguito per primo. Non importa se il compilatore cambia queste due righe in t1. Anche se il compilatore non ha cambiato quelle istruzioni, la tua Console.WriteLne nel ramo else potrebbe comunque essere eseguita, anche CON la parola chiave volatile su _flag.
Jakotheshadows l'

@jakotheshadows, hai ragione, ho modificato la mia risposta. La mia idea principale era quella di mostrare che la logica attesa poteva essere rotta quando eseguiamo t1 e t2 contemporaneamente
Aliaksei Maniuk

0

Quindi per riassumere tutto ciò, la risposta corretta alla domanda è: se il tuo codice è in esecuzione in runtime 2.0 o versioni successive, la parola chiave volatile non è quasi mai necessaria e fa più male che bene se usata inutilmente. IE Non usarlo mai. MA nelle versioni precedenti del runtime, è necessario per un corretto doppio controllo sui campi statici. In particolare campi statici la cui classe ha un codice di inizializzazione della classe statica.


-4

più thread possono accedere a una variabile. L'ultimo aggiornamento sarà sulla variabile

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.