La necessità di un modificatore volatile nel doppio blocco controllato in .NET

Più testi dicono che quando si implementa il blocco a doppio controllo in .NET, il campo su cui si sta eseguendo il lock dovrebbe avere un modificatore volatile applicato. Ma perché esattamente? Considerando il seguente esempio:

public sealed class Singleton { private static volatile Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return instance; } } } 

perché “lock (syncRoot)” non esegue la necessaria coerenza di memoria? Non è vero che dopo l’enunciazione “lock” sia la lettura che la scrittura sarebbero volatili e quindi la coerenza necessaria sarebbe stata raggiunta?

Volatile non è necessario Bene, una specie di **

volatile è usato per creare una barriera di memoria * tra letture e scritture sulla variabile.
lock , una volta utilizzato, provoca la creazione di barriere di memoria attorno al blocco all’interno del lock , oltre a limitare l’accesso al blocco a un thread.
Le barriere della memoria fanno in modo che ogni thread legga il valore più corrente della variabile (non un valore locale memorizzato nella cache in qualche registro) e che il compilatore non riordini le istruzioni. L’uso di volatile non è necessario ** perché hai già un lucchetto.

Joseph Albahari spiega questa roba molto meglio di quanto potrei mai.

E assicurati di controllare la guida di Jon Skeet per implementare il singleton in C #

aggiornamento :
* volatile fa sì che le letture della variabile siano VolatileRead e le scritture siano VolatileWrite s, che su x86 e x64 su CLR, sono implementate con un MemoryBarrier . Possono essere più fini su altri sistemi.

** la mia risposta è corretta solo se si sta utilizzando il CLR su processori x86 e x64. Potrebbe essere vero in altri modelli di memoria, come su Mono (e altre implementazioni), Itanium64 e hardware futuro. Questo è ciò a cui Jon si riferisce nel suo articolo nel “gotchas” per il blocco a doppio controllo.

È ansible che uno di {segnando la variabile come volatile , leggendolo con Thread.VolatileRead o inserendo una chiamata a Thread.MemoryBarrier } sia necessario che il codice funzioni correttamente in una situazione di modello di memoria debole.

Da quello che ho capito, sul CLR (anche su IA64), le scritture non vengono mai riordinate (le scritture hanno sempre semantica di rilascio). Tuttavia, in IA64, le letture possono essere riordinate per venire prima delle scritture, a meno che non siano contrassegnate come volatili. Sfortunatamente, non ho accesso all’hardware IA64 con cui giocare, quindi qualsiasi cosa che dico a riguardo sarebbe una speculazione.

ho trovato anche questi articoli utili:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
l’articolo di Vance Morrison (tutto ciò si collega a questo, si parla di doppio blocco controllato)
l’articolo di chris brumme (tutto è collegato a questo)
Joe Duffy: Varianti spezzate del bloccaggio a doppio controllo

le serie di luis abreu sul multithreading danno una bella panoramica dei concetti
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx

C’è un modo per implementarlo senza campo volatile . Lo spiegherò …

Penso che sia il riordino dell’accesso alla memoria all’interno del blocco che è pericoloso, in modo da poter ottenere un’istanza inizializzata non completamente al di fuori del blocco. Per evitare questo faccio questo:

 public sealed class Singleton { private static Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { // very fast test, without implicit memory barriers or locks if (instance == null) { lock (syncRoot) { if (instance == null) { var temp = new Singleton(); // ensures that the instance is well initialized, // and only then, it assigns the static variable. System.Threading.Thread.MemoryBarrier(); instance = temp; } } } return instance; } } } 

Capire il codice

Immagina che ci siano alcuni codici di inizializzazione all’interno del costruttore della class Singleton. Se queste istruzioni vengono riordinate dopo che il campo è stato impostato con l’indirizzo del nuovo object, allora hai un’istanza incompleta … immagina che la class abbia questo codice:

 private int _value; public int Value { get { return this._value; } } private Singleton() { this._value = 1; } 

Ora immagina una chiamata al costruttore usando il nuovo operatore:

 instance = new Singleton(); 

Questo può essere esteso a queste operazioni:

 ptr = allocate memory for Singleton; set ptr._value to 1; set Singleton.instance to ptr; 

Cosa succede se riordino queste istruzioni in questo modo:

 ptr = allocate memory for Singleton; set Singleton.instance to ptr; set ptr._value to 1; 

Fa la differenza? NO se pensi a un singolo thread. se pensi a più thread … cosa succede se il thread viene interrotto subito dopo aver set instance to ptr :

 ptr = allocate memory for Singleton; set Singleton.instance to ptr; -- thread interruped here, this can happen inside a lock -- set ptr._value to 1; -- Singleton.instance is not completelly initialized 

Questo è ciò che la barriera della memoria evita, non consentendo il riordino dell’accesso alla memoria:

 ptr = allocate memory for Singleton; set temp to ptr; // temp is a local variable (that is important) set ptr._value to 1; -- memory barrier... cannot reorder writes after this point, or reads before it -- -- Singleton.instance is still null -- set Singleton.instance to temp; 

Buona programmazione!

Non penso che nessuno abbia effettivamente risposto alla domanda , quindi farò un tentativo.

Il volatile e il primo if (instance == null) non sono “necessari”. Il blocco renderà il codice thread-safe.

Quindi la domanda è: perché dovresti aggiungere il primo if (instance == null) ?

Il motivo è presumibilmente per evitare di eseguire inutilmente la sezione di codice bloccata. Mentre si sta eseguendo il codice all’interno del blocco, qualsiasi altro thread che tenta di eseguire anche quel codice viene bloccato, il che rallenterà il programma se si tenta di accedere al singleton frequentemente da molti thread. A seconda della lingua / piattaforma, potrebbero anche esserci delle spese generali dal blocco stesso che si desidera evitare.

Quindi il primo controllo nulla viene aggiunto come un modo molto veloce per vedere se è necessario il blocco. Se non è necessario creare il singleton, è ansible evitare completamente il blocco.

Ma non è ansible verificare se il riferimento è nullo senza bloccarlo in qualche modo, perché a causa della memorizzazione nella cache del processore, un altro thread potrebbe cambiarlo e si leggerà un valore “obsoleto” che potrebbe portare a inserire il blocco inutilmente. Ma stai cercando di evitare una serratura!

Così rendi il singleton volatile per assicurarti di leggere l’ultimo valore, senza dover usare un lucchetto.

Hai ancora bisogno del blocco interno perché solo il volatile ti protegge durante un singolo accesso alla variabile – non puoi testarlo e impostarlo in sicurezza senza usare un lucchetto.

Ora, è davvero utile?

Beh, direi “nella maggior parte dei casi, no”.

Se Singleton.Instance potrebbe causare inefficienza a causa dei blocchi, allora perché lo chiami così frequentemente che questo sarebbe un problema significativo ? L’intero punto di un singleton è che ce n’è solo uno, quindi il tuo codice può leggere e memorizzare nella cache il riferimento singleton una volta.

L’unico caso in cui posso pensare a dove questo caching non sarebbe ansible sarebbe quando si ha un numero elevato di thread (ad esempio un server che utilizza un nuovo thread per elaborare ogni richiesta potrebbe creare milioni di thread a esecuzione molto breve, ognuno dei quali che dovrebbe chiamare Singleton.Instance una volta).

Quindi sospetto che il double check locking sia un meccanismo che ha un posto reale in specifici casi critici per le prestazioni, e poi ognuno si è arrampicato sul “questo è il modo giusto di farlo” senza pensare veramente a ciò che fa e se è vero sarà effettivamente necessario nel caso lo stiano usando.

AFAIK (e – prendi questo con caucanvas, non sto facendo molte cose simultanee) no. Il blocco ti dà solo la sincronizzazione tra più concorrenti (thread).

volatile d’altra parte dice alla tua macchina di rivalutare il valore ogni volta, in modo da non incappare in un valore memorizzato nella cache (e sbagliato).

Vedi http://msdn.microsoft.com/en-us/library/ms998558.aspx e nota la seguente citazione:

Inoltre, la variabile è dichiarata volatile per garantire che l’assegnazione alla variabile di istanza venga completata prima che sia ansible accedere alla variabile di istanza.

Una descrizione di volatile: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx

Si dovrebbe usare volatile con il modello di blocco a doppio controllo.

La maggior parte delle persone indica questo articolo come prova che non è necessario volatile: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

Ma non riescono a leggere fino alla fine: ” Un’ultima parola di avvertimento – Sto solo indovinando il modello di memoria x86 dal comportamento osservato sui processori esistenti, quindi le tecniche di basso lock sono anche fragili perché hardware e compilatori possono diventare più aggressivi nel tempo Ecco alcune strategie per minimizzare l’impatto di questa fragilità sul codice: in primo luogo, quando ansible, evitare tecniche di basso lock. (…) Infine, assumere il modello di memoria più debole ansible, usando dichiarazioni volatili invece di fare affidamento su garanzie implicite “.

Se hai bisogno di più convincenti, leggi questo articolo sulle specifiche ECMA che verrà utilizzato per altre piattaforms: msdn.microsoft.com/en-us/magazine/jj863136.aspx

Se hai bisogno di ulteriori convincenti leggi questo articolo più recente in cui potrebbero essere inserite ottimizzazioni che impediscono il funzionamento senza volatilità: msdn.microsoft.com/en-us/magazine/jj883956.aspx

In sintesi, “potrebbe” funzionare per voi senza volatilità per il momento, ma non rischiare di scrivere codice corretto e utilizzare metodi volatili o volatilever / scrittura. Gli articoli che suggeriscono di fare altrimenti stanno a volte lasciando fuori alcuni dei possibili rischi di ottimizzazioni JIT / compilatore che potrebbero avere un impatto sul tuo codice, così come le future ottimizzazioni che potrebbero accadere che potrebbero infrangere il tuo codice. Come già accennato ipotesi nell’ultimo articolo, precedenti ipotesi di lavorare senza volatilità potrebbero non reggere su ARM.

Il lock è sufficiente. La specifica del linguaggio MS (3.0) menziona esso stesso questo esatto scenario nel §8.12, senza alcuna menzione di volatile :

Un approccio migliore consiste nel sincronizzare l’accesso ai dati statici bloccando un object statico privato. Per esempio:

 class Cache { private static object synchronizationObject = new object(); public static void Add(object x) { lock (Cache.synchronizationObject) { ... } } public static void Remove(object x) { lock (Cache.synchronizationObject) { ... } } } 

Penso di aver trovato quello che stavo cercando. I dettagli sono in questo articolo – http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10 .

Per riassumere, in .NET il modificatore volatile non è davvero necessario in questa situazione. Tuttavia nei modelli di memoria più deboli le scritture fatte nel costruttore di oggetti inizializzati possono essere ritardate dopo la scrittura sul campo, quindi altri thread potrebbero leggere istanze non nulle corrotte nella prima istruzione if.

Questo è un buon post sull’utilizzo di volatile con il doppio controllo bloccato:

http://tech.puredanger.com/2007/06/15/double-checked-locking/

In Java, se l’objective è proteggere una variabile, non è necessario bloccarla se è contrassegnata come volatile