Volatile vs. Interbloccato contro blocco

Supponiamo che una class abbia un campo public int counter cui si accede da più thread. Questo int è solo incrementato o decrementato.

Per incrementare questo campo, quale approccio dovrebbe essere utilizzato e perché?

  • lock(this.locker) this.counter++; ,
  • Interlocked.Increment(ref this.counter); ,
  • Cambia il modificatore di accesso del counter a public volatile .

Ora che ho scoperto volatile , ho rimosso molte dichiarazioni di lock e l’uso di Interlocked . Ma c’è una ragione per non farlo?

Peggiore (in realtà non funzionerà)

Cambia il modificatore di accesso del counter a public volatile

Come altre persone hanno menzionato, questo da solo non è affatto sicuro. Il punto di volatile è che più thread in esecuzione su più CPU possono memorizzare e memorizzare dati e riordinare le istruzioni.

Se non è volatile e la CPU A incrementa un valore, la CPU B potrebbe non vedere quel valore incrementato fino a qualche tempo dopo, il che potrebbe causare problemi.

Se è volatile , ciò garantisce solo che le due CPU vedano gli stessi dati nello stesso momento. Non impedisce loro di interlacciare le loro letture e operazioni di scrittura, che è il problema che stai cercando di evitare.

Il secondo migliore:

lock(this.locker) this.counter++ ;

Questo è sicuro da fare (a condizione che ti ricordi di lock ovunque tu acceda a this.counter ). Impedisce a qualsiasi altro thread di eseguire qualsiasi altro codice che è custodito locker . Anche l’uso dei blocchi impedisce i problemi di riordino della multi-CPU come sopra, il che è fantastico.

Il problema è che il blocco è lento e se si riutilizza l’ locker in qualche altro posto che non è realmente correlato, si può finire per bloccare gli altri thread senza motivo.

Migliore

Interlocked.Increment(ref this.counter);

Questo è sicuro, poiché fa effettivamente leggere, incrementare e scrivere in “un colpo” che non può essere interrotto. Per questo motivo, non influirà su nessun altro codice e non è necessario ricordarsi di bloccarlo altrove. È anche molto veloce (come dice MSDN, sulle moderne CPU, questa è spesso letteralmente una singola istruzione della CPU).

Non sono completamente sicuro, tuttavia, se si aggira le altre CPU per riordinare le cose, o se è anche necessario combinare volatile con l’incremento.

InterlockedNotes:

  1. I METODI INTERBLOCCATI SONO CONTINUAMENTE SICURI SU QUALSIASI NUMERO DI CORE O CPU.
  2. I metodi interbloccati applicano una barriera completa attorno alle istruzioni che eseguono, quindi il riordino non avviene.
  3. I metodi interbloccati non hanno bisogno o addirittura non supportano l’accesso a un campo volatile , poiché volatile è posto a metà della recinzione attorno alle operazioni su un determinato campo e l’interblocco sta utilizzando la recinzione completa.

Nota a piè di pagina: per cosa è volatile?

Come volatile non impedisce questo tipo di problemi di multithreading, a cosa serve? Un buon esempio è che hai due thread, uno che scrive sempre su una variabile (ad esempio queueLength ) e uno che legge sempre dalla stessa variabile.

Se queueLength non è volatile, il thread A può scrivere cinque volte, ma il thread B potrebbe vedere tali scritture come ritardate (o anche potenzialmente nell’ordine sbagliato).

Una soluzione sarebbe quella di bloccare, ma potresti anche usare volatile in questa situazione. Ciò assicurerebbe che il thread B vedrà sempre la cosa più aggiornata che il thread A ha scritto. Nota comunque che questa logica funziona solo se hai scrittori che non leggono mai, e lettori che non scrivono mai, e se la cosa che stai scrivendo è un valore atomico. Non appena fai un singolo read-modify-write, devi andare alle operazioni interbloccate o usare un Lock.

EDIT: Come notato nei commenti, in questi giorni sono felice di usare Interlocked per i casi di una singola variabile dove ovviamente è okay. Quando diventerà più complicato, tornerò al blocco …

L’utilizzo di volatile non aiuta quando è necessario incrementare, poiché la lettura e la scrittura sono istruzioni separate. Un altro thread potrebbe cambiare il valore dopo aver letto ma prima di rispondere.

Personalmente quasi sempre mi blocco – è più facile ottenere il giusto in un modo che è ovviamente giusto rispetto a volatilità o Interlocked.Increment. Per quanto mi riguarda, il multi-threading lock-free è per veri esperti di threading, di cui non sono uno. Se Joe Duffy e il suo team costruivano delle belle librerie che parallelassero le cose senza tanto bloccare come qualcosa che avrei creato, è favoloso, e lo userò in un batter d’occhio – ma quando sto facendo il thread me stesso, cerco di mantienilo semplice

volatile ” non sostituisce Interlocked.Increment ! Si limita a fare in modo che la variabile non sia memorizzata nella cache, ma utilizzata direttamente.

L’incremento di una variabile richiede in realtà tre operazioni:

  1. leggere
  2. incremento
  3. Scrivi

Interlocked.Increment esegue tutte e tre le parti come una singola operazione atomica.

Il blocco o l’incremento interbloccato è ciò che stai cercando.

Volatile non è sicuramente quello che stai cercando – dice semplicemente al compilatore di considerare la variabile come sempre variabile anche se il percorso corrente del codice consente al compilatore di ottimizzare una lettura dalla memoria altrimenti.

per esempio

 while (m_Var) { } 

se m_Var è impostato su false in un altro thread ma non è dichiarato come volatile, il compilatore è libero di renderlo un ciclo infinito (ma non significa che lo farà sempre) facendolo controllare contro un registro della CPU (ad es. EAX perché quello era ciò che m_Var è stato inserito fin dall’inizio) invece di emettere un’altra lettura nella posizione di memoria di m_Var (questo potrebbe essere memorizzato nella cache – non lo sappiamo e non ci interessa e questo è il punto di coerenza della cache di x86 / x64). Tutti i post precedenti di altri che hanno menzionato il riordino delle istruzioni mostrano semplicemente che non capiscono le architetture x86 / x64. Volatile non emette barriere di lettura / scrittura come implicito nei post precedenti che dicono “previene il riordino”. Infatti, grazie ancora al protocollo MESI, siamo sicuri che il risultato che leggiamo è sempre lo stesso tra le CPU, indipendentemente dal fatto che i risultati effettivi siano stati ritirati nella memoria fisica o semplicemente risiedano nella cache della CPU locale. Non andrò troppo lontano nei dettagli di questo, ma mi assicuro che se ciò dovesse andare storto, Intel / AMD probabilmente emetterebbe un richiamo del processore! Questo significa anche che non ci dobbiamo preoccupare dell’esecuzione fuori ordine ecc. I risultati sono sempre garantiti per andare in pensione in ordine – altrimenti siamo imbottiti!

Con Increment Interlocked, il processore deve uscire, recuperare il valore dall’indirizzo fornito, quindi incrementarlo e riscriverlo – tutto ciò pur avendo la proprietà esclusiva dell’intera linea della cache (lock xadd) per assicurarsi che nessun altro processore possa modificare il suo valore

Con volatile, ti ritroverai comunque con solo 1 istruzione (supponendo che il JIT sia efficiente come dovrebbe) – inc dword ptr [m_Var]. Tuttavia, il processore (cpuA) non richiede la proprietà esclusiva della linea cache mentre fa tutto ciò che ha fatto con la versione interbloccata. Come puoi immaginare, questo significa che altri processori potrebbero scrivere un valore aggiornato su m_Var dopo che è stato letto da cpuA. Quindi, invece di aver incrementato il valore due volte, si finisce con una sola volta.

Spero che questo chiarisca il problema.

Per maggiori informazioni, vedere ‘Comprendere l’impatto delle tecniche Low-Lock nelle app multithreaded’ – http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

ps Che cosa ha spinto questa risposta molto tarda? Tutte le risposte sono state così palesemente errate (specialmente quella segnata come risposta) nella loro spiegazione. Ho dovuto solo chiarirlo per chiunque altro leggesse questo. alza le spalle

pps Suppongo che il target sia x86 / x64 e non IA64 (ha un modello di memoria diverso). Si noti che le specifiche ECMA di Microsoft sono rovinate dal fatto che specifica il modello di memoria più debole invece di quello più forte (è sempre meglio specificarlo rispetto al modello di memoria più forte in modo coerente con le piattaforms – altrimenti codice che verrebbe eseguito 24-7 su x86 / x64 potrebbe non funzionare affatto su IA64 sebbene Intel abbia implementato un modello di memoria altrettanto forte per IA64) – Microsoft lo ha ammesso personalmente – http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .

Le funzioni interbloccate non si bloccano. Sono atomici, ovvero possono essere completati senza la possibilità di un cambio di contesto durante l’incremento. Quindi non c’è possibilità di stallo o aspettare.

Direi che dovresti sempre preferirlo a un blocco e a un incremento.

Volatile è utile se hai bisogno che le scritture in un thread vengano lette in un altro, e se vuoi che lo ottimizzatore non riordini le operazioni su una variabile (perché le cose accadono in un altro thread di cui l’ottimizzatore non sa nulla). È una scelta ortogonale al modo in cui aumenti.

Questo è davvero un buon articolo se vuoi saperne di più sul codice senza blocco e sul modo giusto di approcciarlo scrivendolo

http://www.ddj.com/hpc-high-performance-computing/210604448

lock (…) funziona, ma potrebbe bloccare un thread e causare deadlock se altri codici utilizzano gli stessi lock in modo incompatibile.

Interlocked. * È il modo corretto di farlo … molto meno di un sovraccarico dato che le moderne CPU lo supportano come un primitivo.

volatile da solo non è corretto. Un thread che tenta di recuperare e quindi riscrivere un valore modificato potrebbe comunque entrare in conflitto con un altro thread che fa lo stesso.

In secondo luogo la risposta di Jon Skeet e voglio aggiungere i seguenti link per tutti coloro che vogliono saperne di più su “volatile” e Interlocked:

Atomicità, volatilità e immutabilità sono diverse, prima parte – (Fabulous Adventures In Coding di Eric Lippert)

Atomicità, volatilità e immutabilità sono diverse, seconda parte

Atomicità, volatilità e immutabilità sono diverse, parte tre

Sayonara Volatile – (istantanea di Wayback Machine del Weblog di Joe Duffy come è apparso nel 2012)

Ho fatto qualche prova per vedere come funziona effettivamente la teoria: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . Il mio test era più focalizzato su CompareExchnage ma il risultato per Increment è simile. Interlocked non è necessario più veloce nell’ambiente multi-CPU. Ecco il risultato del test per Increment su un server CPU di 16 anni. Ricordatevi che il test comporta anche la lettura sicura dopo l’aumento, che è tipica nel mondo reale.

 D:\>InterlockVsMonitor.exe 16 Using 16 threads: InterlockAtomic.RunIncrement (ns): 8355 Average, 8302 Minimal, 8409 Maxmial MonitorVolatileAtomic.RunIncrement (ns): 7077 Average, 6843 Minimal, 7243 Maxmial D:\>InterlockVsMonitor.exe 4 Using 4 threads: InterlockAtomic.RunIncrement (ns): 4319 Average, 4319 Minimal, 4321 Maxmial MonitorVolatileAtomic.RunIncrement (ns): 933 Average, 802 Minimal, 1018 Maxmial 

Leggi il riferimento Threading in C # . Copre i dettagli della tua domanda. Ognuno dei tre ha diversi scopi ed effetti collaterali.