it-swarm.it

Quando deve essere usata la parola chiave volatile in C #?

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

279
Doron Yaacoby

Non penso che ci sia una persona migliore per rispondere a questo rispetto a Eric Lippert (enfasi nell'originale): 

In C #, "volatile" significa non solo "assicurarsi che il compilatore e il jitter Non eseguano alcun codice di riordino o registrazione di cache Ottimizzazioni su questa variabile". Significa anche "dire ai processori di fare Fare tutto ciò che è necessario fare per assicurarsi di leggere l'ultimo valore Anche se ciò significa arrestare altri processori e fare Loro sincronizzano la memoria principale con i loro cache".

In realtà, quell'ultimo bit è una bugia. La vera semantica delle letture volatili e le scritture sono considerevolmente più complesse di quelle che ho delineato qui; nel infatti non garantiscono effettivamente che ogni processore interrompa ciò che è sta facendo e aggiorna le cache nella/dalla memoria principale. Piuttosto, forniscono garanzie più deboli su come la memoria accede prima e dopo le letture e le scritture possono essere osservate per essere ordinate l'una rispetto all'altra . Alcune operazioni come la creazione di un nuovo thread, l'inserimento di un blocco o l'uso di uno dei metodi della famiglia Interlocked introduce più forte garanzie sull'osservazione dell'ordine. Se vuoi maggiori dettagli, leggere le sezioni 3.10 e 10.5.3 della specifica C # 4.0.

Francamente, ti scoraggio dal fare un campo volatile. Volatile i campi sono un segno che stai facendo qualcosa di completamente pazzo: sei tentando di leggere e scrivere lo stesso valore su due thread differenti senza mettere un blocco in posizione. I blocchi garantiscono che la lettura della memoria o modificato all'interno della serratura è osservato per essere coerente, garantisce serrature che solo un thread accede a un determinato blocco di memoria alla volta e così sopra. Il numero di situazioni in cui un blocco è troppo lento è molto piccolo, e la probabilità che hai intenzione di ottenere il codice sbagliato perché non capisci l'esatto modello di memoria è molto grande. IO non tentare di scrivere alcun codice di blocco basso tranne il più banale usi di operazioni interbloccate. Lascio l'uso di "volatile" a veri esperti.

Per ulteriori letture vedi:

254
Ohad Schneider

Se vuoi ottenere un po 'più tecnico su cosa 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 del compilatore ottimizzate standard (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 ciclo non volatile (il primo) il compilatore ha assegnato i al registro eax. Abbastanza semplice. Ci sono un paio di bit interessanti però - l'istruzione lea ebx, [ebx] è effettivamente un'istruzione multibyte nop 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 del ciclo invece di usare un'istruzione inc eax. L'istruzione 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 ciclo con il contatore di 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 arriva addirittura a non eseguire un load/increment/store come tre distinti passi (load eax, inc eax, save eax) quando si aggiorna il valore del contatore, invece la memoria viene modificata direttamente in una singola istruzione (un add mem , reg). Il modo in cui il codice è stato creato garantisce che il valore del contatore del ciclo sia sempre aggiornato nel contesto di un singolo core della CPU. Nessuna operazione sui dati può causare corruzione o perdita di dati (quindi non utilizzare il carico/inc/store poiché il valore può cambiare durante l'inc in modo da essere perso nel negozio). Poiché gli interrupt possono essere riparati solo una volta completata l'istruzione corrente, i dati non possono mai essere danneggiati, anche con memoria non allineata.

Una volta introdotta una seconda CPU nel sistema, la parola chiave volatile non proteggerà i dati che vengono aggiornati da un'altra CPU nello stesso momento. Nell'esempio precedente, i dati devono essere non allineati per ottenere un potenziale danneggiamento. La parola chiave volatile non impedirà il potenziale danneggiamento se i dati non possono essere gestiti in modo atomico, ad esempio se il contatore di loop era di tipo long long (64 bit), allora richiederebbe due operazioni a 32 bit per aggiornare il valore, nel mezzo di quale può verificarsi un interrupt e modificare i dati.

Quindi, la parola chiave volatile è valida solo per i dati allineati che è inferiore o uguale alla dimensione dei registri nativi, in modo tale che le operazioni siano sempre atomiche.

La parola chiave volatile è stata concepita per essere utilizzata con IO operazioni in cui IO cambiano continuamente ma hanno un indirizzo costante, ad esempio un dispositivo mappato in memoria UART, e il compilatore non dovrebbe continua a riutilizzare il primo valore letto dall'indirizzo.

Se gestisci 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.

54
Skizz

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 potrebbe causare un secondo thread per accedere a un oggetto non null, ma non completamente costruito:

  1. Thread 1 chiede se una variabile è null . // if (this.foo == null)
  2. Thread 1 determina che la variabile è null, quindi inserisce un lock . // lock (this.bar)
  3. Thread 1 chiede di nuovo se la variabile è null . // 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, a questo.foo poteva essere assegnata la nuova istanza di Foo, prima che il costruttore fosse finito in esecuzione. In questo caso, potrebbe entrare un secondo thread (durante la chiamata di thread 1 al costruttore di Foo) e provare quanto segue:

  1. Thread 2 chiede se la variabile è nulla. // if (this.foo == null)
  2. Thread 2 determina che la variabile NON è null, quindi prova ad usarlo . // this.foo.MakeFoo ()

Prima di .NET 2.0, è possibile dichiarare questo.foo come volatile per ovviare a questo problema. Dal momento che .NET 2.0, non è più necessario utilizzare la parola chiave volatile per eseguire il blocco con doppio controllo.

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

38
AndrewTek

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

22
Dr. Bob

A volte, il compilatore ottimizza un campo e usa un registro per memorizzarlo. Se il thread 1 esegue una scrittura nel campo e un altro thread lo accede, poiché l'aggiornamento è stato memorizzato in un registro (e non in memoria), il secondo thread otterrebbe dati obsoleti.

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

20
Benoit

Il CLR ama ottimizzare le istruzioni, quindi quando si accede a un campo nel codice potrebbe non accedere sempre al valore corrente del campo (potrebbe essere dallo stack, ecc.). Contrassegnare un campo come volatile assicura che il valore corrente del campo sia accessibile dall'istruzione. Ciò è utile quando il valore può essere modificato (in uno scenario non bloccante) da un thread concorrente nel programma o da un altro codice in esecuzione nel sistema operativo.

Ovviamente perdi qualche ottimizzazione, ma mantiene il codice più semplice.

13
Joseph Daigle

Il compilatore a volte cambia 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 multi-thread. Guarda l'esempio seguente:

 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 output o "Value: 10" come risultato. Potrebbe essere che il compilatore cambi linea nella funzione t1. Se t2 viene eseguito, potrebbe essere che _flag abbia valore di 5, ma _value ha 0. Quindi la logica prevista potrebbe essere interrotta. 

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

private static volatile int _flag = 0;

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

0
Aliaksei Maniuk

Quindi, per riassumere tutto ciò, 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. OSSIA Non usarlo mai. MA nelle versioni precedenti del runtime, è necessario IS per il corretto controllo del doppio controllo sui campi statici. Campi specificatamente statici la cui classe ha il codice di inizializzazione della classe statica.

0
Paul Easter