Ho letto alcuni articoli sulla volatile
parola chiave ma non sono riuscito a capirne il corretto utilizzo. Potresti dirmi per cosa dovrebbe essere usato in C # e Java?
Ho letto alcuni articoli sulla volatile
parola chiave ma non sono riuscito a capirne il corretto utilizzo. Potresti dirmi per cosa dovrebbe essere usato in C # e Java?
Risposte:
Sia per C # che per Java, "volatile" dice al compilatore che il valore di una variabile non deve mai essere memorizzato nella cache poiché il suo valore può cambiare al di fuori dell'ambito del programma stesso. Il compilatore eviterà quindi eventuali ottimizzazioni che potrebbero 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, in questo modo:
System.out.println(5);
Tuttavia, se esiste un altro thread che può cambiare i
, questo è un comportamento sbagliato. Se un altro thread i
diventa 6, la versione ottimizzata continuerà a stampare 5.
La volatile
parola chiave impedisce tale ottimizzazione e memorizzazione nella cache ed è quindi utile quando una variabile può essere modificata da un altro thread.
i
contrassegnato come volatile
. In Java si tratta di relazioni che accadono prima .
i
è una variabile locale, nessun altro thread può modificarla comunque. Se è un campo, il compilatore non può ottimizzare la chiamata a meno che non lo sia final
. Non penso che il compilatore possa fare ottimizzazioni basandosi sul presupposto che un campo "sembri" final
quando non è esplicitamente dichiarato come tale.
Per capire cosa fa una variabile volatile, è importante capire cosa succede quando la variabile non è volatile.
Quando due thread A e B accedono a una variabile non volatile, ogni thread manterrà una copia locale della variabile nella sua cache locale. Qualsiasi modifica apportata dal thread A nella sua cache locale non sarà visibile al thread B.
Quando le variabili vengono dichiarate volatili, ciò significa essenzialmente che i thread non devono memorizzare nella cache tale variabile o, in altre parole, i thread non devono fidarsi dei valori di queste variabili a meno che non vengano letti direttamente dalla memoria principale.
Quindi, quando rendere volatile una variabile?
Quando si dispone di una variabile a cui è possibile accedere da molti thread e si desidera che ogni thread ottenga l'ultimo valore aggiornato di quella variabile anche se il valore viene aggiornato da qualsiasi altro thread / processo / esterno al programma.
Le letture dei campi volatili hanno acquisito la semantica . Ciò significa che è garantito che la memoria letta dalla variabile volatile si verificherà prima che venga letta qualsiasi memoria successiva. Impedisce al compilatore di eseguire il riordino e, se l'hardware lo richiede (CPU debolmente ordinata), utilizzerà un'istruzione speciale per fare in modo che l'hardware svuoti tutte le letture che si verificano dopo la lettura volatile ma sono state avviate speculativamente in anticipo, oppure la CPU potrebbe impedire che vengano emessi in anticipo in primo luogo, impedendo che si verifichi qualsiasi 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 eventuali scritture di memoria sulla variabile volatile possano essere ritardate fino a quando tutte le precedenti scritture di memoria sono visibili ad altri processori.
Considera il seguente esempio:
something.foo = new Thing();
Se foo
è una variabile membro in una classe e altre CPU hanno accesso all'istanza dell'oggetto a cui fa riferimento something
, potrebbero vedere il foo
cambiamento del valore prima che le scritture di memoria nel Thing
costruttore siano visibili globalmente! Questo è ciò che significa "memoria debolmente ordinata". Ciò potrebbe verificarsi anche se il compilatore ha tutti i negozi nel costruttore prima del negozio foo
. In foo
questo caso volatile
il negozio foo
avrà la semantica di rilascio e l'hardware garantisce che tutte le scritture prima della scrittura foo
siano visibili ad altri processori prima di consentire l'esecuzione della scrittura foo
.
Come è possibile che le scritture foo
vengano riordinate così male? Se il mantenimento della riga della cache si foo
trova nella cache e gli archivi nel costruttore hanno perso la cache, è possibile che l'archivio si completi molto prima delle mancate scritture nella cache.
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 famosissimo ARMv7-A, hanno una memoria debolmente ordinata.
Gli sviluppatori spesso non vedono queste corse di dati perché cose come i blocchi faranno una barriera di memoria piena, essenzialmente la stessa cosa dell'acquisizione e del rilascio della semantica allo stesso tempo. Nessun carico all'interno del blocco può essere eseguito in modo speculativo prima che il blocco venga acquisito, vengono ritardati fino all'acquisizione del blocco. Nessun archivio può essere ritardato attraverso un rilascio del blocco, l'istruzione che rilascia il blocco viene ritardata fino a quando tutte le scritture eseguite all'interno del blocco sono visibili globalmente.
Un esempio più completo è il modello "Blocco doppio controllo". Lo scopo di questo modello è di evitare di acquisire sempre un lucchetto per inizializzare un oggetto in modo pigro.
Preso 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 MySingleton
costruttore potrebbero non essere visibili ad altri processori prima del negozio mySingleton
. Se ciò accade, gli altri thread che danno un'occhiata a mySingleton non acquisiranno un blocco e non necessariamente raccoglieranno le scritture per il costruttore.
volatile
non impedisce mai la memorizzazione nella cache. Ciò che fa è garantire l'ordine in cui gli altri processori "vedono" le scritture. Un rilascio di un negozio ritarderà un negozio fino al completamento di tutte le scritture in sospeso e un ciclo di bus è stato emesso dicendo ad altri processori di scartare / riscrivere la loro linea di cache se le cache pertinenti sono memorizzate nella cache. Un'acquisizione di carico annulla tutte le letture speculate, assicurando che non siano valori obsoleti del passato.
head
e tail
devono essere volatili per impedire al produttore di assumere tail
che non cambierà e per impedire al consumatore di assumere head
che non cambierà. Inoltre, head
deve essere volatile per garantire che le scritture dei dati della coda siano visibili a livello globale prima che l'archivio head
sia visibile a livello globale.
La parola chiave volatile ha significati diversi sia in Java che in C #.
Dalle specifiche del linguaggio Java :
Un campo può essere dichiarato volatile, nel qual caso il modello di memoria Java garantisce che tutti i thread visualizzino un valore coerente per la variabile.
Dal riferimento C # sulla parola chiave volatile :
La parola chiave volatile indica che un campo può essere modificato nel programma tramite qualcosa come il sistema operativo, l'hardware o un thread che esegue contemporaneamente.
In Java, "volatile" viene utilizzato per indicare alla JVM che la variabile può essere utilizzata da più thread contemporaneamente, quindi 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 per la CPU memorizzare nella cache in modo aggressivo i dati in suo possesso perché l'accesso alla memoria è molto più lento dell'accesso alla cache. Ciò significa che se i dati vengono aggiornati nella CPU1 devono passare immediatamente attraverso tutte le cache e nella memoria principale anziché quando la cache decide di svuotarsi, 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'oggetto è volatile, il thread ottiene sempre il valore più aggiornato.
Volatile sta risolvendo il problema di concorrenza. Per rendere quel valore sincronizzato. Questa parola chiave viene utilizzata principalmente in un threading. Quando più thread aggiornano la stessa variabile.