Domanda

Supponiamo che una classe abbia un campo contatore int pubblico a cui accedono più thread. Questo int viene 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 del contatore in public volatile .

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

È stato utile?

Soluzione

Peggiore (in realtà non funzionerà)

  

Cambia il modificatore di accesso del contatore in public volatile

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

Se non è 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.

Se è volatile , ciò garantisce solo che le due CPU vedano 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.

Secondo migliore:

  

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

Questo è sicuro (purché ti ricordi di bloccare ovunque tu acceda a this.counter ). Impedisce a qualsiasi altro thread di eseguire qualsiasi altro codice protetto da locker . L'uso dei blocchi impedisce anche i problemi di riordino della multi-CPU come sopra, il che è fantastico.

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

Best

  

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 è anche necessario combinare volatile con l'incremento.

InterlockedNotes:

  1. I METODI INTERBLOCCATI SONO SICURAMENTE SICURI SU QUALSIASI NUMERO DI NUCLEI O CPU.
  2. I metodi interbloccati applicano una barriera completa attorno alle istruzioni che eseguono, quindi il riordino non avviene.
  3. 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 interbloccato utilizza l'intero recinto.

Nota in calce: ciò che è volatile è in realtà un bene.

Dato che volatile 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 in una variabile (come 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 solo funziona 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 esegui una sola lettura-modifica-scrittura, devi andare alle operazioni interbloccate o utilizzare un blocco.

Altri suggerimenti

EDIT: Come notato nei commenti, in questi giorni sono felice di usare Interlocked per i casi di una singola variabile dove è < em> ovviamente ok. Quando diventa più complicato, tornerò ancora al blocco ...

L'uso di volatile non 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 andare bene in un modo ovviamente giusto rispetto alla volatilità o Interlocked.Increment. 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.

" volatili " 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. leggi
  2. incremento
  3. scrittura

Interlocked.Increment esegue tutte e tre le parti come un'unica operazione atomica.

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 altrimenti.

per es.

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 contro un registro CPU (ad es. EAX perché era quello che m_Var è stato recuperato fin dall'inizio) invece di dare un'altra lettura alla 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 non emette 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 dall'ordine, ecc. I risultati sono sempre garantiti per andare in pensione in ordine - altrimenti saremo riempiti!

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 (lock xadd) per assicurarsi che nessun altro i processori possono modificarne il 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

P.S. 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. si stringe nelle spalle

p.p.s. 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 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 forte per IA64) - Microsoft ha ammesso questo stesso - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .

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 leggere le scritture in un thread 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 al modo in cui aumenti.

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

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

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 un primitivo.

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.

Ho fatto alcuni test per vedere come funziona davvero 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

Vorrei aggiungere a menzionato nelle altre risposte la differenza tra volatile , Interlocked e lock :

La parola chiave volatile può essere applicata ai 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 altro parole, non puoi dichiarare un " pointer " essere "volatile".
  • Tipi semplici come sbyte , byte , short , ushort , int , uint , char , float e bool .
  • Un tipo enum con uno dei seguenti tipi base: byte , sbyte , short , ushort, int o uint .
  • Parametri di tipo generico noti come tipi di riferimento.
  • IntPtr e UIntPtr .

Altri tipi , inclusi double e long , non possono essere contrassegnati " volatile " perché non è possibile garantire la lettura e la scrittura in campi di questi tipi essere atomico. Per proteggere l'accesso multi-thread a questi tipi di , usa i membri della classe Interlocked o proteggi l'accesso usando il lock .

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top