A cosa serve la parola chiave "volatile"?


130

Ho letto alcuni articoli sulla volatileparola chiave ma non sono riuscito a capirne il corretto utilizzo. Potresti dirmi per cosa dovrebbe essere usato in C # e Java?


1
Uno dei problemi con volatile è che significa più di una cosa. Essere informazioni al compilatore per non fare ottimizzazioni funky è un'eredità C. Significa anche che le barriere di memoria dovrebbero essere utilizzate all'accesso. Ma nella maggior parte dei casi costa solo prestazioni e / o confonde le persone. : P
AnorZaken,

Risposte:


93

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".


@Tom - debitamente notato, signore - e modificato.
Sarà un

11
È ancora molto più sottile di così.
Tom Hawtin - tackline

1
Sbagliato. Non impedisce la memorizzazione nella cache. Vedi la mia risposta
doug65536,

168

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 idiventa 6, la versione ottimizzata continuerà a stampare 5.

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


3
Credo che l'ottimizzazione sarebbe ancora valida con icontrassegnato come volatile. In Java si tratta di relazioni che accadono prima .
Tom Hawtin - tackline

Grazie per la pubblicazione, quindi in qualche modo volatile ha connessioni con blocco variabile?
Mircea,

@Mircea: Questo è ciò che mi è stato detto che contrassegnare qualcosa come volatile era tutto: contrassegnare un campo come volatile avrebbe usato un meccanismo interno per consentire ai thread di vedere un valore coerente per la variabile data, ma questo non è menzionato nella risposta sopra ... forse qualcuno può confermarlo o no? Grazie
npinti,

5
@Sjoerd: non sono sicuro di aver capito questo esempio. Se 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" finalquando non è esplicitamente dichiarato come tale.
poligenelubrificanti

1
C # e java non sono C ++. Questo non è corretto Non impedisce la memorizzazione nella cache e non impedisce l'ottimizzazione. Si tratta della semantica di lettura-acquisizione e di rilascio del negozio, che sono richieste su architetture di memoria debolmente ordinate. Riguarda l'esecuzione speculativa.
doug65536,

40

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

  • 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.

  • La variabile è volatile

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.


2
Sbagliato. Non ha nulla a che fare con "impedire la memorizzazione nella cache". Si tratta di riordinare, tramite il compilatore, o l'hardware della CPU attraverso l'esecuzione speculativa.
doug65536,

37

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 foocambiamento del valore prima che le scritture di memoria nel Thingcostruttore 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 fooquesto caso volatileil negozio fooavrà la semantica di rilascio e l'hardware garantisce che tutte le scritture prima della scrittura foosiano visibili ad altri processori prima di consentire l'esecuzione della scrittura foo.

Come è possibile che le scritture foovengano riordinate così male? Se il mantenimento della riga della cache si footrova 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 MySingletoncostruttore 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.

volatilenon 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.


Buona spiegazione Anche un buon esempio di blocco doppio controllo. Tuttavia, non sono ancora sicuro su quando utilizzare poiché sono preoccupato per gli aspetti della cache. Se scrivo un'implementazione di coda in cui verrà scritto solo 1 thread e solo 1 thread verrà letto, posso cavarmela senza blocchi e contrassegnare i "puntatori" di testa e coda come volatili? Voglio garantire che sia il lettore che lo scrittore vedano i valori più aggiornati.
Nickdu,

Entrambi heade taildevono essere volatili per impedire al produttore di assumere tailche non cambierà e per impedire al consumatore di assumere headche non cambierà. Inoltre, headdeve essere volatile per garantire che le scritture dei dati della coda siano visibili a livello globale prima che l'archivio headsia visibile a livello globale.
doug65536,

+1, Termini come ultimo / "più aggiornato" purtroppo implicano un concetto del singolare valore corretto. In realtà due concorrenti possono tagliare un traguardo nello stesso momento - su una cpu due core possono richiedere una scrittura nello stesso momento . Dopotutto, i core non si alternano nel fare il lavoro, il che renderebbe multi-core inutile. Un buon pensiero / design multi-thread non dovrebbe concentrarsi sul tentativo di forzare la "novità" di basso livello - intrinsecamente falsa poiché un blocco forza solo i core a selezionare arbitrariamente un altoparlante alla volta senza equità - ma piuttosto cerca di progettare necessità di un concetto così innaturale.
AnorZaken

34

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

Giava

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.

C #

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.


Grazie mille per la pubblicazione, come ho capito in Java, si comporta come il blocco di quella variabile in un contesto di thread, e in C # se usato il valore della variabile può essere modificato non solo dal programma, fattori esterni come il sistema operativo possono modificarne il valore ( nessuna chiusura implicita) ... Per favore fatemi sapere se ho capito bene quelle differenze ...
Mircea,

@Mircea in Java non è coinvolto alcun blocco, assicura solo che verrà utilizzato il valore più aggiornato della variabile volatile.
Krock,

Java promette una sorta di barriera di memoria, o è come C ++ e C # solo promettendo di non ottimizzare il riferimento?
Steven Sudit,

La barriera di memoria è un dettaglio di implementazione. Ciò che Java effettivamente promette è che tutte le letture vedranno il valore scritto dalla scrittura più recente.
Stephen C,

1
@StevenSudit Sì, se l'hardware richiede una barriera o carica / acquisisce o memorizza / rilascia, utilizzerà tali istruzioni. Vedi la mia risposta
doug65536,

9

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).


1

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.


1
Puoi riformulare la tua risposta?
Anirudha Gupta,

la parola chiave volatile ti darà il valore più aggiornato anziché il valore memorizzato nella cache.
Subhash Saini,

0

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.


1
Non credo che "risolva" il problema. È uno strumento che aiuta in alcune circostanze. Non fare affidamento su volatile per le situazioni in cui è necessario un blocco, come in una condizione di gara.
Scratte il
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.