A cosa serve la parola chiave “volatile”?

Ho letto alcuni articoli sulla parola chiave volatile ma non sono riuscito a capire il suo uso corretto. Potresti dirmi per che cosa dovrebbe essere usato in C # e in Java?

Per C # e Java, “volatile” indica al compilatore che il valore di una variabile non deve mai essere memorizzato nella cache poiché il suo valore potrebbe cambiare al di fuori dell’ambito del programma stesso. Il compilatore eviterà quindi qualsiasi ottimizzazione che potrebbe causare problemi se la variabile cambia “al di fuori del suo controllo”.

Considera questo esempio:

 int i = 5; System.out.println(i); 

Il compilatore può ottimizzare questo per stampare solo 5, come questo:

 System.out.println(5); 

Tuttavia, se c’è un altro thread che può cambiare i , questo è il comportamento sbagliato. Se un altro thread cambia i 6, la versione ottimizzata continuerà a stampare 5.

La parola chiave volatile impedisce tale ottimizzazione e memorizzazione nella cache, e quindi è utile quando una variabile può essere modificata da un altro thread.

La parola chiave volatile ha significati diversi sia in Java che in C #.

Giava

Dalla specifica del linguaggio Java :

Un campo può essere dichiarato volatile, nel qual caso il modello di memoria Java garantisce che tutti i thread vedano un valore coerente per la variabile.

C #

Dal riferimento C # sulla parola chiave volatile :

La parola chiave volatile indica che un campo può essere modificato nel programma da qualcosa come il sistema operativo, l’hardware o un thread in esecuzione simultaneo.

Per capire che cosa è volatile per una variabile, è importante capire cosa succede quando la variabile non è volatile.

  • La variabile non è volatile

Quando due thread A e B stanno accedendo a una variabile non volatile, ogni thread manterrà una copia locale della variabile nella propria cache locale. Eventuali modifiche apportate dal thread A nella cache locale non saranno visibili al thread B.

  • La variabile è volatile

Quando le variabili sono dichiarate volatili, significa essenzialmente che i thread non devono memorizzare nella cache una variabile di questo tipo o in altre parole i thread non dovrebbero fidarsi dei valori di queste variabili a meno che non siano letti direttamente dalla memoria principale.

Quindi, quando rendere variabile una variabile?

Quando si dispone di una variabile a cui è ansible accedere da più thread e si desidera che ogni thread ottenga l’ultimo valore aggiornato di tale variabile anche se il valore viene aggiornato da qualsiasi altro thread / processo / esterno al programma.

Le letture di campi volatili hanno acquisito la semantica . Ciò significa che è garantito che la memoria letta dalla variabile volatile si verificherà prima di qualsiasi lettura successiva della memoria. Blocca il compilatore dal fare il riordino, e se l’hardware lo richiede (CPU debolmente ordinata), userà un’istruzione speciale per fare in modo che l’hardware svuota le letture che si verificano dopo la lettura volatile ma sono state avviate speculativamente in anticipo, o la CPU potrebbe impedire che vengano emesse all’inizio, in primo luogo, impedendo che si verifichi un carico speculativo tra l’emissione del carico acquisito e il suo ritiro.

Le scritture di campi volatili hanno una semantica di rilascio . Ciò significa che è garantito che tutte le scritture di memoria nella variabile volatile vengano ritardate fino a quando tutte le scritture precedenti della memoria non sono visibili agli altri processori.

Considera il seguente esempio:

 something.foo = new Thing(); 

Se foo è una variabile membro in una class e altre CPU hanno accesso all’istanza dell’object a cui fa riferimento something , potrebbero vedere il valore foo change prima che le scritture di memoria nel costruttore Thing siano visibili a livello globale! Questo è ciò che significa “memoria debolmente ordinata”. Questo potrebbe accadere anche se il compilatore ha tutti i negozi nel costruttore prima che il negozio foo . Se foo è volatile lo store to foo avrà una semantica di rilascio, e l’hardware garantisce che tutte le scritture prima della scrittura su foo siano visibili agli altri processori prima di consentire la scrittura su foo .

Com’è ansible che le scritture di foo vengano riordinate così male? Se la riga della cache che tiene premuto foo è nella cache e gli archivi nel costruttore mancano la cache, allora è ansible che lo store completi molto prima delle scritture sulle cache mancanti.

La (terribile) architettura Itanium di Intel aveva debolmente ordinato memoria. Il processore utilizzato nell’XBox 360 originale aveva una memoria debolmente ordinata. Molti processori ARM, incluso il molto popolare ARMv7-A, hanno una memoria debolmente ordinata.

Gli sviluppatori spesso non vedono questi dati razze perché cose come i lucchetti faranno una barriera di memoria completa, essenzialmente la stessa cosa di acquisire e rilasciare semantica allo stesso tempo. Nessun carico all’interno del blocco può essere eseguito speculativamente prima dell’acquisizione del blocco, essi vengono ritardati fino a quando non viene acquisito il blocco. Nessun archivio può essere ritardato attraverso un rilascio di blocco, l’istruzione che rilascia il blocco viene ritardata fino a quando tutte le scritture effettuate all’interno del blocco sono globalmente visibili.

Un esempio più completo è il pattern “Double-checked locking”. Lo scopo di questo modello è di evitare di dover sempre acquisire un blocco per inizializzare un object in modo pigro.

Impiccato da Wikipedia:

 public class MySingleton { private static object myLock = new object(); private static volatile MySingleton mySingleton = null; private MySingleton() { } public static MySingleton GetInstance() { if (mySingleton == null) { // 1st check lock (myLock) { if (mySingleton == null) { // 2nd (double) check mySingleton = new MySingleton(); // Write-release semantics are implicitly handled by marking mySingleton with // 'volatile', which inserts the necessary memory barriers between the constructor call // and the write to mySingleton. The barriers created by the lock are not sufficient // because the object is made visible before the lock is released. } } } // The barriers created by the lock are not sufficient because not all threads will // acquire the lock. A fence for read-acquire semantics is needed between the test of mySingleton // (above) and the use of its contents.This fence is automatically inserted because mySingleton is // marked as 'volatile'. return mySingleton; } } 

In questo esempio, i negozi nel costruttore MySingleton potrebbero non essere visibili ad altri processori prima del negozio su mySingleton . Se ciò accade, gli altri thread che fanno capolino su mySingleton non acquisiscono un lock e non necessariamente raccoglieranno le scritture nel costruttore.

volatile non impedisce mai il caching. Quello che fa è garantire l’ordine in cui gli altri processori “vedono” scrive. Una release del negozio ritarderà un negozio fino a quando tutte le scritture in sospeso non saranno complete e sarà stato emesso un ciclo di bus che dice agli altri processori di scartare / riscrivere la propria linea di cache se si verificano che le righe pertinenti siano memorizzate nella cache. Un carico acquisito svuoterà tutte le letture speculate, assicurando che non saranno valori obsoleti dal passato.

In Java, “volatile” viene utilizzato per indicare alla JVM che la variabile può essere utilizzata da più thread contemporaneamente, pertanto alcune ottimizzazioni comuni non possono essere applicate.

In particolare la situazione in cui i due thread che accedono alla stessa variabile sono in esecuzione su CPU separate nella stessa macchina. È molto comune che le CPU memorizzino in modo aggressivo i dati che contiene poiché l’accesso alla memoria è molto più lento dell’accesso alla cache. Ciò significa che se i dati vengono aggiornati in CPU1, deve immediatamente passare attraverso tutte le cache e la memoria principale invece di quando la cache decide di cancellare se stessa, in modo che CPU2 possa vedere il valore aggiornato (di nuovo ignorando tutte le cache sulla strada).

Quando si leggono dati non volatili, il thread in esecuzione può o non può sempre ottenere il valore aggiornato. Ma se l’object è volatile, il thread ottiene sempre il valore più aggiornato.