L’accesso a una variabile in C # è un’operazione atomica?

Sono stato convinto che se più thread possono accedere a una variabile, tutte le letture e le scritture su quella variabile devono essere protette dal codice di sincronizzazione, ad esempio un’istruzione “lock”, perché il processore potrebbe passare a un altro thread a metà uno scritto.

Tuttavia, stavo guardando attraverso System.Web.Security.Membership usando Reflector e trovato codice come questo:

public static class Membership { private static bool s_Initialized = false; private static object s_lock = new object(); private static MembershipProvider s_Provider; public static MembershipProvider Provider { get { Initialize(); return s_Provider; } } private static void Initialize() { if (s_Initialized) return; lock(s_lock) { if (s_Initialized) return; // Perform initialization... s_Initialized = true; } } } 

Perché il campo s_Initialized è letto all’esterno della serratura? Non è ansible che un altro thread provi a scriverlo nello stesso momento? Le letture e le scritture delle variabili sono atomiche?

Per la risposta definitiva vai alle specifiche. 🙂

La partizione I, sezione 12.6.6 della specifica CLI afferma: “Una CLI conforms garantisce che l’accesso in lettura e scrittura a posizioni di memoria correttamente allineate non più grandi della dimensione nativa della parola sia atomico quando tutti gli accessi in scrittura a una posizione sono della stessa dimensione “.

In tal modo si conferma che s_Initialized non sarà mai instabile e che leggere e scrivere per primitve i tipi più piccoli di 32 bit sono atomici.

In particolare, double e long ( Int64 e UInt64 ) non sono garantiti per essere atomici su una piattaforma a 32 bit. È ansible utilizzare i metodi sulla class Interlocked per proteggerli.

Inoltre, mentre le letture e le scritture sono atomiche, esiste una condizione di competizione con addizione, sottrazione e tipi primitivi di incremento e decremento, poiché devono essere letti, gestiti e riscritti. La class interbloccata ti consente di proteggerli usando i metodi CompareExchange e Increment .

L’interblocco crea una barriera di memoria per impedire al processore di riordinare letture e scritture. Il blocco crea l’unica barriera richiesta in questo esempio.

Questa è una (ctriggers) forma del modello di blocco a doppio controllo che non è thread-safe in C #!

C’è un grosso problema in questo codice:

s_Initialized non è volatile. Ciò significa che le scritture nel codice di inizializzazione possono spostarsi dopo che s_Initialized è impostato su true e altri thread possono vedere il codice non inizializzato anche se s_Initialized è true per loro. Questo non si applica all’implementazione del Framework da parte di Microsoft perché ogni scrittura è una scrittura volatile.

Ma anche nell’implementazione di Microsoft, le letture dei dati non inizializzati possono essere riordinate (cioè precaricate dalla cpu), quindi se s_Initialized è vero, la lettura dei dati da inizializzare può risultare nella lettura di dati vecchi e non inizializzati a causa di hit della cache (es. le letture sono riordinate).

Per esempio:

 Thread 1 reads s_Provider (which is null) Thread 2 initializes the data Thread 2 sets s\_Initialized to true Thread 1 reads s\_Initialized (which is true now) Thread 1 uses the previously read Provider and gets a NullReferenceException 

Spostare la lettura di s_Provider prima della lettura di s_Initialized è perfettamente legale perché non c’è nessuna lettura volatile da nessuna parte.

Se s_Initialized fosse volatile, la lettura di s_Provider non avrebbe il permesso di spostarsi prima della lettura di s_Initialized e anche l’inizializzazione del Provider non può spostarsi dopo che s_Initialized sia impostato su true e tutto è ok ora.

Anche Joe Duffy ha scritto un articolo su questo problema: varianti rotte sul blocco a doppio controllo

Aspetta un po ‘- la domanda che è nel titolo non è sicuramente la vera domanda che Rory sta chiedendo.

La domanda titolare ha la semplice risposta di “No” – ma questo non aiuta affatto, quando vedi la vera domanda – a cui non penso che nessuno abbia dato una risposta semplice.

La vera domanda che Rory chiede è presentata molto più tardi ed è più pertinente all’esempio che dà.

Perché il campo s_Initialized è letto all’esterno della serratura?

La risposta a questo è anche semplice, anche se completamente estraneo all’atomicità dell’accesso variabile.

Il campo s_Initialized viene letto all’esterno del blocco perché i lock sono costosi .

Poiché il campo s_Initialized è essenzialmente “scrivi una volta”, non restituirà mai un falso positivo.

È economico leggerlo fuori dalla serratura.

Questa è un’attività a basso costo con un’alta probabilità di avere un vantaggio.

Ecco perché viene letto fuori dalla serratura – per evitare di pagare il costo dell’uso di un lucchetto a meno che non sia indicato.

Se le serrature fossero economiche, il codice sarebbe più semplice e omettere quel primo controllo.

(modifica: la bella risposta di rory segue: Yeh, le letture booleane sono molto atomiche.Se qualcuno ha costruito un processore con letture booleane non atomiche, sarebbero presenti sul DailyWTF.)

La risposta corretta sembra essere, “Sì, soprattutto.”

  1. La risposta di John che fa riferimento alla specifica CLI indica che gli accessi a variabili non superiori a 32 bit su un processore a 32 bit sono atomici.
  2. Ulteriore conferma dalle specifiche C #, sezione 5.5, Atomicità dei riferimenti variabili :

    Le letture e le scritture dei seguenti tipi di dati sono atomiche: bool, char, byte, sbyte, short, ushort, uint, int, float e reference. Inoltre, le letture e le scritture dei tipi di enum con un tipo sottostante nell’elenco precedente sono anche atomiche. Le letture e le scritture di altri tipi, inclusi i tipi lunghi, ulong, double e decimal, così come i tipi definiti dall’utente, non sono garantiti come atomici.

  3. Il codice nel mio esempio è stato parafrasato dalla class Membership, come scritto dal team ASP.NET stesso, quindi era sempre sicuro assumere che il modo in cui accede al campo s_Initialized sia corretto. Ora sappiamo perché.

Modifica: come sottolinea Thomas Danecker, anche se l’accesso al campo è atomico, s_Initialized dovrebbe essere contrassegnato come volatile per assicurarsi che il blocco non venga interrotto dal processore che riordina le letture e le scritture.

La funzione di inizializzazione è difettosa. Dovrebbe assomigliare più a questo:

 private static void Initialize() { if(s_initialized) return; lock(s_lock) { if(s_Initialized) return; s_Initialized = true; } } 

Senza il secondo controllo all’interno del blocco è ansible che il codice di inizializzazione venga eseguito due volte. Quindi il primo controllo è per le prestazioni per risparmiare l’ s_Initialized un blocco inutilmente, e il secondo controllo è per il caso in cui un thread sta eseguendo il codice di inizializzazione ma non ha ancora impostato il flag s_Initialized e quindi un secondo thread passerebbe il primo controllo e stai aspettando alla serratura.

Le letture e le scritture delle variabili non sono atomiche. È necessario utilizzare le API di sincronizzazione per emulare letture / scritture atomiche.

Per un fantastico riferimento su questo e molti altri problemi relativi alla concorrenza, assicurati di prendere una copia dell’ultimo spettacolo di Joe Duffy. È uno squartatore!

“L’accesso a una variabile in C # è un’operazione atomica?”

No. E non è una cosa di C #, né è nemmeno una cosa .net, è una cosa da processore.

OJ è il punto su cui Joe Duffy è il ragazzo con cui andare per questo tipo di informazioni. E “interbloccato” è un ottimo termine da utilizzare se vuoi saperne di più.

“Le letture violente” possono verificarsi su qualsiasi valore i cui campi si sumno a più della dimensione di un puntatore.

@Leon
Vedo il tuo punto di vista – il modo in cui l’ho chiesto, e poi commentato, la domanda permette di essere preso in un paio di modi diversi.

Per essere chiari, volevo sapere se era sicuro che i thread concorrenti leggessero e scrivessero su un campo booleano senza alcun codice di sincronizzazione esplicito, cioè accedendo a un atomico variabile booleano (o altro primitivo).

Poi ho usato il codice Membership per dare un esempio concreto, ma questo ha introdotto un sacco di distrazioni, come il blocco a doppio controllo, il fatto che s_Initialized è sempre impostato una sola volta e che ho commentato il codice di inizializzazione stesso.

Colpa mia.

Puoi anche decorare s_Initialized con la parola chiave volatile e rinunciare completamente all’uso del blocco.

Non è corretto Si verificherà ancora il problema di un secondo thread che passa il controllo prima che il primo thread abbia avuto la possibilità di impostare il flag, il che comporterà più esecuzioni del codice di inizializzazione.

Penso che tu stia chiedendo se s_Initialized potrebbe essere in uno stato instabile quando viene letto fuori dalla serratura. La risposta breve è no. Un semplice compito / lettura si riduce ad una singola istruzione di assemblaggio che è atomica su ogni processore a cui riesco a pensare.

Non sono sicuro di quale sia il caso per l’assegnazione a variabili a 64 bit, dipende dal processore, presumo che non sia atomico ma probabilmente si tratta di moderni processori a 32 bit e certamente su tutti i processori a 64 bit. L’assegnazione di tipi di valore complessi non sarà atomica.

Pensavo che fossero – Non sono sicuro del punto del blocco nel tuo esempio, a meno che tu non stia facendo qualcosa anche con s_Provider allo stesso tempo – quindi il blocco assicurerebbe che queste chiamate siano avvenute insieme.

//Perform initialization commento di //Perform initialization copre la creazione di s_Provider? Per esempio

 private static void Initialize() { if (s_Initialized) return; lock(s_lock) { s_Provider = new MembershipProvider ( ... ) s_Initialized = true; } } 

Altrimenti, la proprietà statica-get restituirà null in ogni caso.

Forse Interlocked dà un indizio. E per il resto questo è abbastanza buono.

Avrei immaginato che non fossero atomici.

Per fare in modo che il tuo codice funzioni sempre su architetture debolmente ordinate, devi mettere un MemoryBarrier prima di scrivere s_Initialized.

 s_Provider = new MemershipProvider; // MUST PUT BARRIER HERE to make sure the memory writes from the assignment // and the constructor have been wriitten to memory // BEFORE the write to s_Initialized! Thread.MemoryBarrier(); // Now that we've guaranteed that the writes above // will be globally first, set the flag s_Initialized = true; 

Le scritture di memoria che avvengono nel costruttore di MembershipProvider e la scrittura su s_Provider non sono garantite prima che si scriva su s_Initialized su un processore debolmente ordinato.

Un sacco di pensiero in questa discussione riguarda se qualcosa è atomico o meno. Questo non è il problema. Il problema è l’ordine in cui le scritture del thread sono visibili ad altri thread . Su architetture debolmente ordinate, le scritture su memoria non avvengono in ordine e QUESTO è il vero problema, non se una variabile si adatta al bus dati.

EDIT: In realtà, sto mixando piattaforms nelle mie dichiarazioni. In C # le specifiche CLR richiedono che le scritture siano globalmente visibili, in ordine (utilizzando le costose istruzioni del negozio per ogni negozio, se necessario). Pertanto, non è necessario avere effettivamente quella barriera di memoria lì. Tuttavia, se fosse C o C ++ in cui non esiste alcuna garanzia di ordine di visibilità globale e la piattaforma di destinazione potrebbe avere una memoria debolmente ordinata ed è multithread, allora è necessario assicurarsi che le scritture dei costruttori siano globalmente visibili prima di aggiornare s_Initialized , che viene testato al di fuori del blocco.

An If (itisso) { controllare su un booleano è atomico, ma anche se non fosse non è necessario bloccare il primo check.

Se qualche thread ha completato l’inizializzazione, allora sarà vero. Non importa se molti thread stanno controllando in una sola volta. Avranno tutti la stessa risposta, e non ci saranno conflitti.

Il secondo controllo all’interno del blocco è necessario perché un altro thread potrebbe aver prima afferrato il blocco e completato il processo di inizializzazione.

Quello che stai chiedendo è se accedere a un campo in un metodo più volte atomico – a cui la risposta è no.

Nell’esempio sopra, la routine di inizializzazione è errata in quanto potrebbe causare l’inizializzazione multipla. Dovresti controllare il flag s_Initialized all’interno del blocco e all’esterno, per evitare una condizione di competizione in cui più thread leggono il flag s_Initialized prima che uno di essi faccia effettivamente il codice di inizializzazione. Per esempio,

 private static void Initialize() { if (s_Initialized) return; lock(s_lock) { if (s_Initialized) return; s_Provider = new MembershipProvider ( ... ) s_Initialized = true; } } 

Ack, non importa … come sottolineato, questo è davvero sbagliato. Non impedisce a un secondo thread di entrare nella sezione di codice “initialize”. Bah.

Puoi anche decorare s_Initialized con la parola chiave volatile e rinunciare completamente all’uso del blocco.