Quando usare la parola chiave volatile in C #

Le tecniche di ottimizzazione utilizzate dal compilatore JIT (just-in-time) in Common Language Runtime potrebbero portare a risultati imprevedibili quando il programma .Net sta tentando di eseguire letture non volatili di dati in uno scenario multithread. In questo articolo esamineremo le differenze tra l'accesso alla memoria volatile e non volatile, il ruolo della parola chiave volatile in C # e come dovrebbe essere utilizzata la parola chiave volatile.

Fornirò alcuni esempi di codice in C # per illustrare i concetti. Per capire come funziona la parola chiave volatile, dobbiamo prima capire come funziona la strategia di ottimizzazione del compilatore JIT in .Net.

Comprensione delle ottimizzazioni del compilatore JIT

Va notato che il compilatore JIT, come parte di una strategia di ottimizzazione, cambierà l'ordine delle letture e delle scritture in un modo che non cambia il significato e l'eventuale output del programma. Ciò è illustrato nello snippet di codice riportato di seguito.

x = 0;

x = 1;

Lo snippet di codice sopra può essere modificato come segue, preservando la semantica originale del programma.

x = 1;

Il compilatore JIT può anche applicare un concetto chiamato "propagazione costante" per ottimizzare il codice seguente.

x = 1;

y = x;

Lo snippet di codice sopra può essere modificato come segue, ancora una volta preservando la semantica originale del programma.

x = 1;

y = 1;

Accesso alla memoria volatile e non volatile

Il modello di memoria dei sistemi moderni è piuttosto complicato. Hai registri del processore, vari livelli di cache e memoria principale condivisa da più processori. Quando il programma viene eseguito, il processore può memorizzare nella cache i dati e quindi accedere a questi dati dalla cache quando viene richiesto dal thread in esecuzione. Gli aggiornamenti e le letture di questi dati potrebbero essere eseguiti sulla versione dei dati memorizzata nella cache, mentre la memoria principale viene aggiornata in un secondo momento. Questo modello di utilizzo della memoria ha conseguenze per le applicazioni multithread. 

Quando un thread interagisce con i dati nella cache e un secondo thread tenta di leggere gli stessi dati contemporaneamente, il secondo thread potrebbe leggere una versione obsoleta dei dati dalla memoria principale. Questo perché quando il valore di un oggetto non volatile viene aggiornato, la modifica viene apportata nella cache del thread in esecuzione e non nella memoria principale. Tuttavia, quando il valore di un oggetto volatile viene aggiornato, non solo la modifica viene apportata nella cache del thread in esecuzione, ma questa cache viene quindi scaricata nella memoria principale. E quando il valore di un oggetto volatile viene letto, il thread aggiorna la sua cache e legge il valore aggiornato.

Utilizzo della parola chiave volatile in C #

La parola chiave volatile in C # viene utilizzata per informare il compilatore JIT che il valore della variabile non deve mai essere memorizzato nella cache perché potrebbe essere modificato dal sistema operativo, dall'hardware o da un thread in esecuzione simultanea. Il compilatore evita così di utilizzare eventuali ottimizzazioni sulla variabile che potrebbero portare a conflitti di dati, cioè a thread differenti che accedono a valori differenti della variabile.

Quando si contrassegna un oggetto o una variabile come volatile, diventa un candidato per letture e scritture volatili. Va notato che in C # tutte le scritture in memoria sono volatili indipendentemente dal fatto che si scrivano dati su un oggetto volatile o non volatile. Tuttavia, l'ambiguità si verifica durante la lettura dei dati. Quando si leggono dati non volatili, il thread in esecuzione può o meno ottenere sempre il valore più recente. Se l'oggetto è volatile, il thread ottiene sempre il valore più aggiornato.

È possibile dichiarare una variabile come volatile facendola precedere dalla volatileparola chiave. Lo snippet di codice seguente lo illustra.

programma di classe

    {

        pubblico volatile int i;

        static void Main (string [] args)

        {

            // Scrivi qui il tuo codice

        }

    }

È possibile utilizzare la volatileparola chiave con qualsiasi tipo di riferimento, puntatore e enumerazione. È inoltre possibile utilizzare il modificatore volatile con i tipi byte, short, int, char, float e bool. Va notato che le variabili locali non possono essere dichiarate volatili. Quando si specifica un oggetto di tipo riferimento come volatile, solo il puntatore (un intero a 32 bit che punta alla posizione in memoria in cui l'oggetto è effettivamente memorizzato) è volatile, non il valore dell'istanza. Inoltre, una doppia variabile non può essere volatile perché ha una dimensione di 64 bit, maggiore della dimensione della parola sui sistemi x86. Se hai bisogno di rendere volatile una doppia variabile, dovresti racchiuderla in classe. Puoi farlo facilmente creando una classe wrapper come mostrato nello snippet di codice di seguito.

classe pubblica VolatileDoubleDemo

{

    volatile privato WrappedVolatileDouble volatileData;

}

classe pubblica WrappedVolatileDouble

{

    doppi dati pubblici {get; impostato; }

Tuttavia, notare la limitazione dell'esempio di codice precedente. Sebbene si disponga del valore più recente del volatileDatapuntatore di riferimento, non è garantito l'ultimo valore della Dataproprietà. Il modo per aggirare questo problema consiste nel rendere il WrappedVolatileDoubletipo immutabile.

Sebbene la parola chiave volatile possa aiutarti nella sicurezza dei thread in determinate situazioni, non è una soluzione a tutti i tuoi problemi di concorrenza dei thread. Dovresti sapere che contrassegnare una variabile o un oggetto come volatile non significa che non devi usare la parola chiave lock. La parola chiave volatile non sostituisce la parola chiave lock. Serve solo per aiutarti a evitare conflitti di dati quando più thread tentano di accedere agli stessi dati.