Volatile vs. Interbloccato vs. blocco


671

Diciamo che una classe ha un public int countercampo a cui accedono più thread. Questo intè solo incrementato o decrementato.

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

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

Ora che ho scoperto volatile, ho rimosso molte lockdichiarazioni e l'uso di Interlocked. Ma c'è un motivo per non farlo?


Leggi il threading nel riferimento C # . Copre i dettagli della tua domanda. Ognuno dei tre ha scopi ed effetti collaterali diversi.
spoulson,

1
simple-talk.com/blogs/2012/01/24/… puoi vedere l'uso di volitive negli array, non lo capisco completamente, ma è un ulteriore riferimento a ciò che fa.
Eran Otzap,

50
È come dire "Ho scoperto che il sistema antincendio non viene mai attivato, quindi lo rimuoverò e lo sostituirò con allarmi antincendio". Il motivo per non farlo è perché è incredibilmente pericoloso e non ti dà quasi alcun beneficio . Se hai tempo da dedicare alla modifica del codice, trova un modo per renderlo meno multithread ! Non trovi un modo per rendere il codice multithread più pericoloso e facilmente infrangibile!
Eric Lippert,

1
La mia casa ha sia sprinkler e rilevatori di fumo. Quando si incrementa un contatore su un thread e lo si legge su un altro, sembra che sia necessario un lock (o un Interlocked) e una parola chiave volatile. Verità?
yoyo

2
@yoyo No, non hai bisogno di entrambi.
David Schwartz,

Risposte:


859

Peggiore (in realtà non funzionerà)

Cambia il modificatore di accesso di counterapublic volatile

Come altri hanno già detto, questo da solo non è affatto sicuro. Il punto volatileè che più thread in esecuzione su più CPU possono e memorizzeranno nella cache i dati e riordineranno le istruzioni.

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

In tal caso volatile, ciò garantisce che le due CPU visualizzino gli stessi dati contemporaneamente. Non li impedisce affatto di intercalare le loro operazioni di lettura e 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 in lockqualsiasi altro luogo a cui accedi this.counter). Impedisce a qualsiasi altro thread di eseguire qualsiasi altro codice protetto da locker. Utilizzando anche i blocchi, si evitano i problemi di riordino multi-CPU come sopra, il che è fantastico.

Il problema è che il blocco è lento e se riutilizzi lockerin qualche altro posto che non è realmente correlato, puoi finire per bloccare gli altri thread senza motivo.

Migliore

Interlocked.Increment(ref this.counter);

Questo è sicuro, in quanto esegue effettivamente la lettura, l'incremento e la scrittura in "un colpo" che non può essere interrotto. Per questo motivo, non influirà su nessun altro codice e non è necessario ricordare di bloccare altrove. È anche molto veloce (come dice MSDN, sulle moderne CPU, questa è spesso letteralmente una singola istruzione CPU).

Non sono del tutto sicuro, tuttavia, se aggira le altre CPU riordinando le cose o se devi anche combinare volatile con l'incremento.

InterlockedNotes:

  1. I METODI INTERBLOCCATI SONO SICURAMENTE SICURI SU QUALSIASI NUMERO DI CORE O CPU.
  2. I metodi interbloccati applicano una recinzione completa attorno alle istruzioni che eseguono, quindi il riordino non avviene.
  3. I metodi interbloccati non necessitano o addirittura non supportano l'accesso a un campo volatile , poiché volatile viene posizionato un mezzo recinto attorno alle operazioni su un determinato campo e l'interblocco utilizza il recinto completo.

Nota in calce: ciò che è volatile è effettivamente utile.

Come volatile che non impedisce questo tipo di problemi con il multithreading, a cosa serve? Un buon esempio sta nel dire che hai due thread, uno che scrive sempre su una variabile (diciamo queueLength) e uno che legge sempre da quella stessa variabile.

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

Una soluzione sarebbe quella di bloccare, ma potresti anche usare volatile in questa situazione. Ciò garantirebbe che il thread B vedrà sempre la cosa più aggiornata che il thread A ha scritto. Nota tuttavia 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 ha un valore atomico. Non appena si esegue una sola lettura-modifica-scrittura, è necessario accedere alle operazioni interbloccate o utilizzare un blocco.


29
"Non sono del tutto sicuro ... se è necessario combinare anche volatile con l'incremento." Non possono essere combinati AFAIK, in quanto non possiamo passare un volatile dal rif. Ottima risposta a proposito.
Hosam Aly,

41
Grazie mille! La tua nota a piè di pagina su "Ciò che è volatile è effettivamente utile" è ciò che stavo cercando e ha confermato come voglio usare volatile.
Jacques Bosch,

7
In altre parole, se un var viene dichiarato volatile, il compilatore supporrà che il valore del var non rimarrà lo stesso (cioè volatile) ogni volta che il codice lo incontra. Quindi in un ciclo come: while (m_Var) {} e m_Var è impostato su false in un altro thread, il compilatore non controllerà semplicemente ciò che è già in un registro precedentemente caricato con il valore di m_Var ma legge il valore da m_Var ancora. Tuttavia, ciò non significa che la mancata dichiarazione di volatile causerà il loop all'infinito - la specifica di volatile garantisce che non lo farà se m_Var è impostato su false in un altro thread.
Zach vide il

35
@Zach Saw: sotto il modello di memoria per C ++, volatile è il modo in cui l'hai descritto (sostanzialmente utile per la memoria mappata dal dispositivo e non molto altro). Sotto il modello di memoria per CLR (questa domanda è taggata C #) è che volatile inserirà barriere di memoria attorno a letture e scritture in quella posizione di memoria. Le barriere di memoria (e le speciali varianti bloccate di alcune istruzioni di assemblaggio) ti dicono al processore di non riordinare le cose, e sono abbastanza importanti ...
Orion Edwards

19
@ZachSaw: un campo volatile in C # impedisce al compilatore C # e al compilatore jit di effettuare determinate ottimizzazioni che memorizzerebbero il valore nella cache. Fornisce inoltre alcune garanzie su quale ordine di lettura e scrittura può essere osservato su più thread. Come dettaglio di implementazione può farlo introducendo barriere di memoria in lettura e scrittura. La semantica precisa garantita è descritta nelle specifiche; notare che la specifica non garantisce che tutti i thread rispettino un ordinamento coerente di tutte le scritture e letture volatili .
Eric Lippert,

147

EDIT: Come notato nei commenti, in questi giorni sono felice di usare Interlockedper i casi di una singola variabile in cui ovviamente va bene. Quando diventa più complicato, tornerò ancora al blocco ...

L'uso volatilenon aiuta quando è necessario incrementare, poiché la lettura e la scrittura sono istruzioni separate. Un altro thread potrebbe modificare il valore dopo aver letto ma prima di riscrivere.

Personalmente quasi sempre mi blocco - è più facile ottenere il giusto in un modo che ovviamente è giusto rispetto alla volatilità o all'interblocco. Per quanto mi riguarda, il multi-threading senza blocco è per veri esperti di threading, di cui non sono uno. Se Joe Duffy e il suo team costruiscono belle librerie che parallelizzeranno le cose senza bloccare quanto qualcosa che costruirei, è favoloso, e lo userò in un batter d'occhio - ma quando sto facendo il threading io stesso, provo a mantienilo semplice.


16
+1 per avermi assicurato di dimenticare la codifica senza blocco da ora.
Xaqron,

5
i codici senza lock non sono sicuramente veramente senza lock dato che si bloccano in qualche fase - sia a livello di bus (FSB) che a livello di interCPU, c'è ancora una penalità che dovresti pagare. Tuttavia, il blocco a questi livelli inferiori è generalmente più veloce purché non si saturi la larghezza di banda del punto in cui si verifica il blocco.
Zach vide il

2
Non c'è niente di sbagliato in Interlocked, è esattamente quello che stai cercando e più veloce di un blocco completo ()
Jaap

5
@Jaap: Sì, in questi giorni mi sarebbe usare asservita per un vero e proprio singolo contatore. Semplicemente non vorrei iniziare a fare casino cercando di elaborare interazioni tra più aggiornamenti senza blocco alle variabili.
Jon Skeet,

6
@ZachSaw: il tuo secondo commento dice che le operazioni interbloccate "bloccano" ad un certo punto; il termine "blocco" implica generalmente che un'attività può mantenere il controllo esclusivo di una risorsa per un periodo di tempo illimitato; il vantaggio principale della programmazione senza lock è che evita il pericolo che le risorse diventino inutilizzabili a causa del fatto che l'attività proprietaria viene fatta perdere. La sincronizzazione del bus utilizzata dalla classe interbloccata non è solo "generalmente più veloce" - sulla maggior parte dei sistemi ha un limite limitato nel caso peggiore, mentre i blocchi no.
supercat

44

" volatile" non sostituisce Interlocked.Increment! Si assicura solo 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 un'unica operazione atomica.


4
Detto in altro modo, i cambiamenti interbloccati sono completamente recintati e come tali sono atomici. Gli elementi volatili sono solo parzialmente recintati e come tali non sono garantiti come filettati.
JoeGeeky,

1
In realtà, volatilenon si assicura che la variabile non sia memorizzata nella cache. Mette solo restrizioni su come può essere memorizzato nella cache. Ad esempio, può ancora essere memorizzato nella cache della cache L2 della CPU perché sono resi coerenti nell'hardware. Può ancora essere prefetto. Le scritture possono ancora essere postate nella cache e così via. (Il che credo fosse quello a cui stava arrivando Zach.)
David Schwartz

42

O blocco o incremento interbloccato è quello che stai cercando.

Volatile non è sicuramente quello che stai cercando: dice semplicemente al compilatore di trattare la variabile come sempre cambiando anche se il percorso del codice corrente consente al compilatore di ottimizzare una lettura dalla memoria in caso contrario.

per esempio

while (m_Var)
{ }

se m_Var è impostato su false in un altro thread ma non è dichiarato volatile, il compilatore è libero di renderlo un ciclo infinito (ma non significa che lo farà sempre) facendolo verificare su un registro CPU (ad es. EAX perché era in cosa è stato recuperato m_Var sin dall'inizio) invece di rilasciare un'altra lettura nella posizione di memoria di m_Var (questo può 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 comprendono le architetture x86 / x64. Volatile lo fa noemettere barriere di lettura / scrittura come suggerito dai post precedenti che dicono "impedisce il riordino". Infatti, grazie ancora al protocollo MESI, ci viene garantito che il risultato che leggiamo è sempre lo stesso su tutte 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 ti assicuro che se questo va 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 Interlocked Increment, il processore deve uscire, recuperare il valore dall'indirizzo indicato, quindi incrementarlo e riscriverlo - tutto ciò pur avendo la proprietà esclusiva dell'intera riga della cache (blocco xadd) per assicurarsi che nessun altro processore possa modificare il suo valore.

Con volatile, finirai comunque con solo 1 istruzione (supponendo che JIT sia efficiente come dovrebbe) - inc dword ptr [m_Var]. Tuttavia, il processore (cpuA) non richiede la proprietà esclusiva della linea della cache mentre fa tutto ciò che ha fatto con la versione interbloccata. Come puoi immaginare, questo significa che altri processori potrebbero riscrivere 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 risolva il problema.

Per ulteriori informazioni, vedere "Comprensione dell'impatto delle tecniche Low-Lock nelle app multithread" - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

ps Cosa ha spinto questa risposta molto tardi? Tutte le risposte erano così palesemente errate (specialmente quella contrassegnata come risposta) nella loro spiegazione che dovevo solo chiarire per chiunque leggesse questo. alza le spalle

pps Sto assumendo che il target sia x86 / x64 e non IA64 (ha un modello di memoria diverso). Si noti che le specifiche ECMA di Microsoft sono sbagliate in quanto specifica il modello di memoria più debole anziché quello più forte (è sempre meglio specificare contro il modello di memoria più forte in modo che sia coerente tra le piattaforme, 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 potente per IA64) - Microsoft ha ammesso questo stesso - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .


3
Interessante. Puoi fare riferimento a questo? Gradirei volentieri questo voto, ma postare con un linguaggio aggressivo 3 anni dopo una risposta molto votata, coerente con le risorse che ho letto, richiederà una prova un po 'più tangibile.
Steven Evers,

Se riesci a indicare a quale parte vuoi fare riferimento, sarei felice di scavare alcune cose da qualche parte (dubito fortemente di aver nascosto qualsiasi segreto commerciale x86 / x64, quindi questi dovrebbero essere facilmente disponibili su wiki, Intel PRM (manuali di riferimento del programmatore), blog MSFT, MSDN o qualcosa di simile) ...
Zach Saw

2
Perché qualcuno vorrebbe impedire la memorizzazione nella cache della CPU è oltre me. L'intero patrimonio immobiliare (sicuramente non trascurabile in termini di dimensioni e costi) dedicato all'esecuzione della coerenza della cache è completamente sprecato in questo caso ... A meno che non richieda coerenza della cache, come una scheda grafica, un dispositivo PCI ecc., Non imposteresti una riga della cache da scrivere.
Zach vide il

4
Sì, tutto ciò che dici è se non 100% almeno il 99% sul segno. Questo sito è (soprattutto) piuttosto utile quando si è nello slancio dello sviluppo sul lavoro, ma sfortunatamente l'accuratezza delle risposte corrispondenti al (gioco dei) voti non è presente. Quindi, fondamentalmente in StackOverflow, puoi avere un'idea di quale sia la comprensione popolare dei lettori e non di cosa sia realmente. A volte le risposte migliori sono semplicemente incomprensibili: miti di tipo. E sfortunatamente questo è ciò che fa nascere la gente che incontra la lettura mentre risolve il problema. È comprensibile però, nessuno può sapere tutto.
user1416420

1
@BenVoigt Potrei continuare e rispondere a tutte le architetture su cui .NET è in esecuzione, ma ciò richiederebbe alcune pagine e sicuramente non è adatto per SO. È molto meglio educare le persone in base al modello di memoria hardware sottostante .NET maggiormente utilizzato rispetto a quello arbitrario. E con i miei commenti "ovunque", stavo correggendo gli errori che le persone stavano commettendo nell'ipotizzare di svuotare / invalidare la cache ecc. Hanno fatto ipotesi sull'hardware sottostante senza specificare quale hardware.
Zach vide il

16

Le funzioni interbloccate non si bloccano. Sono atomici, nel senso che possono essere completati senza la possibilità di un cambio di contesto durante l'incremento. Quindi non c'è possibilità di deadlock o di attesa.

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

Volatile è utile se è necessario che le scritture in un thread siano lette in un altro e se si desidera che l'ottimizzatore non riordini le operazioni su una variabile (perché le cose stanno accadendo in un altro thread di cui l'ottimizzatore non è a conoscenza). È una scelta ortogonale a come incrementare.

Questo è davvero un buon articolo se vuoi saperne di più sul codice senza blocco e sul modo giusto di avvicinarti a scriverlo

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


11

lock (...) funziona, ma potrebbe bloccare un thread e potrebbe causare deadlock se un altro codice utilizza gli stessi lock in modo incompatibile.

Interbloccato. * È il modo corretto di farlo ... molto meno sovraccarico poiché le moderne CPU lo supportano come una primitiva.

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


8

Ho fatto alcuni test per vedere come funziona 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. L'interblocco non è necessario più velocemente nell'ambiente multi-cpu. Ecco il risultato del test per Increment su un server 16 CPU di 2 anni. Tenete presente che il test prevede anche la lettura sicura dopo l'aumento, che è tipico 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

L'esempio di codice che hai testato è stato davvero banale, ma non ha molto senso testarlo in quel modo! Il migliore sarebbe capire cosa stanno effettivamente facendo i diversi metodi e usare quello appropriato in base allo scenario di utilizzo che hai.
Zach vide il

@Zach, il modo in cui la discussione qui è stata sullo scenario di aumentare un contatore in modo thread sicuro. Quale altro scenario di utilizzo avevi in ​​mente o come lo testeresti? Grazie per il commento BTW.
Kenneth Xu,

Il punto è che è un test artificiale. Non hai intenzione di martellare la stessa posizione che spesso in qualsiasi scenario del mondo reale. Se lo sei, allora bene sei strozzato dall'FSB (come mostrato nelle caselle del server). Comunque, guarda la mia risposta sul tuo blog.
Zach vide il

2
Guardandolo di nuovo indietro. Se il vero collo di bottiglia è con FSB, l'implementazione del monitor dovrebbe osservare lo stesso collo di bottiglia. La vera differenza è che Interlocked sta aspettando e riprovando, il che diventa un vero problema con il conteggio delle prestazioni. Almeno spero che il mio commento porti l'attenzione sul fatto che Interlocked non è sempre la scelta giusta per il conteggio. Il fatto che la gente stia cercando alternative lo ha spiegato bene. Hai bisogno di un lungo sommatore gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…
Kenneth Xu

8

3

Vorrei aggiungere al citato nelle altre risposte la differenza tra volatile, Interlockede lock:

La parola chiave volatile può essere applicata a campi di questi tipi :

  • Tipi di riferimento.
  • Tipi di puntatore (in un contesto non sicuro). Si noti che sebbene il puntatore stesso possa essere volatile, l'oggetto a cui punta non può. In altre parole, non è possibile dichiarare un "puntatore" come "volatile".
  • Tipi semplici come sbyte, byte, short, ushort, int, uint, char, float, e bool.
  • Un tipo enum con uno dei seguenti tipi di base: byte, sbyte, short, ushort, into uint.
  • Parametri di tipo generico noti come tipi di riferimento.
  • IntPtre UIntPtr.

Altri tipi , incluso doublee long, non possono essere contrassegnati come "volatili" perché non è possibile garantire che atomici sia in lettura che in scrittura in campi di tali tipi. Per proteggere l'accesso multi-thread a questi tipi di campi, usa i Interlockedmembri della classe o proteggi l'accesso usando l' lockistruzione.

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.