2010-02-03 13 views
82

Nel mio servizio Web asmx multithreaded avevo un campo classe _allData del mio tipo di SystemData che consiste di pochi List<T> e Dictionary<T> contrassegnati come volatile. I dati di sistema (_allData) vengono aggiornati una volta ogni tanto e lo faccio creando un altro oggetto chiamato newData e riempiendo le sue strutture di dati con nuovi dati. Quando è fatto ho solo assegnareè atomica, quindi perché è necessario Interlocked.Exchange (oggetto ref, oggetto)?

private static volatile SystemData _allData 

public static bool LoadAllSystemData() 
{ 
    SystemData newData = new SystemData(); 
    /* fill newData with up-to-date data*/ 
    ... 
    _allData = newData. 
} 

Questo dovrebbe funzionare in quanto l'assegnazione è atomico e le discussioni che hanno lo riferimento ai vecchi dati continuare a utilizzarlo e il resto hanno i nuovi dati di sistema solo dopo l'assegnazione. Comunque il mio collega ha detto che invece di usare la parola chiave volatile e il semplice assaggiamento dovrei usare InterLocked.Exchange perché ha detto che su alcune piattaforme non è garantito che l'assegnazione di riferimento sia atomica. Inoltre: quando dichiaro the _allData campo come volatile il

Interlocked.Exchange<SystemData>(ref _allData, newData); 

produce avvertimento "un riferimento a un campo volatile non saranno trattati come volatile" Cosa devo pensare a questo?

risposta

141

Qui ci sono numerose domande. Considerandoli uno alla volta:

l'assegnazione di riferimento è atomica, quindi perché è necessario Interlocked.Exchange (oggetto ref, oggetto)?

L'assegnazione di riferimento è atomica. Interlocked.Exchange non esegue solo assegnazioni di riferimento. Fa una lettura del valore corrente di una variabile, ripara il vecchio valore e assegna il nuovo valore alla variabile, tutto come un'operazione atomica.

il mio collega ha affermato che su alcune piattaforme non è garantito che l'assegnazione di riferimento sia atomica. La mia collega era corretta?

No. L'assegnazione di riferimento è garantita per essere atomica su tutte le piattaforme .NET.

Il mio collega ragiona da premesse sbagliate. Ciò significa che le loro conclusioni non sono corrette?

Non necessariamente. Il tuo collega potrebbe darti un buon consiglio per cattive ragioni. Forse c'è qualche altra ragione per cui dovresti usare Interlocked.Exchange. La programmazione senza blocco è faticosamente difficile e nel momento in cui ti allontani da pratiche consolidate sposate da esperti del settore, sei fuori dalle erbacce e rischi il peggior tipo di condizioni di gara. Non sono né un esperto in questo campo né un esperto del tuo codice, quindi non posso esprimere un giudizio in un modo o nell'altro.

produce avviso "un riferimento a un campo volatile non sarà trattato come volatile" Che cosa dovrei pensare a questo?

Si dovrebbe capire perché questo è un problema in generale. Ciò porterà a capire perché l'avvertimento non è importante in questo caso particolare.

Il motivo per cui il compilatore fornisce questo avviso è perché contrassegnare un campo come volatile significa "questo campo verrà aggiornato su più thread - non generare alcun codice che memorizza nella cache i valori di questo campo e assicurarsi che qualsiasi le letture o le scritture di questo campo non vengono "spostate avanti e indietro nel tempo" tramite incoerenze nella cache del processore. "

(Presumo che tu abbia già capito tutto questo Se non hai una conoscenza dettagliata del significato di volatile e di come influisce sulla semantica della cache del processore, allora non capisci come funziona e non dovrebbe essere volatile. I programmi di blocco sono molto difficili da ottenere, assicurati che il tuo programma sia corretto perché capisci come funziona, non per errore.)

Ora supponiamo di creare una variabile che è un alias di un campo volatile di passando un riferimento a quel campo. All'interno del metodo chiamato, il compilatore non ha alcuna ragione per sapere che il riferimento deve avere una semantica volatile! Il compilatore genererà allegramente il codice per il metodo che non riesce a implementare le regole per i campi volatili, ma la variabile è un campo volatile. Ciò può distruggere completamente la logica lock-free; l'ipotesi è sempre che un campo volatile sia sempre a a cui si accede con una semantica volatile. Non ha senso trattarlo come volatile a volte e non altre volte; devi sempre essere coerente altrimenti non puoi garantire la coerenza su altri accessi.

Pertanto, il compilatore avverte quando si esegue questa operazione, poiché probabilmente sta danneggiando completamente la logica lock-free attentamente sviluppata.

Naturalmente, Interlocked.Exchange è scritto per aspettarsi un campo volatile e fare la cosa giusta. L'avvertimento è quindi fuorviante. Mi dispiace molto; ciò che dovremmo fare è implementare un meccanismo in base al quale un autore di un metodo come Interlocked.Exchange potrebbe inserire un attributo nel metodo che dice "questo metodo che prende un riferimento per forzare la semantica volatile sulla variabile, quindi sopprimiamo l'avviso".Forse in una versione futura del compilatore lo faremo.

+4

grazie! la migliore risposta di sempre! –

+0

Da quello che ho sentito Interlocked.Exchange garantisce anche che venga creata una barriera di memoria. Quindi se per esempio create un nuovo oggetto, quindi assegnate un paio di proprietà e quindi memorizzate l'oggetto in un altro riferimento senza usare Interlocked.Exchange, il compilatore potrebbe rovinare l'ordine di quelle operazioni, rendendo così l'accesso al secondo riferimento non thread- sicuro. È davvero così? Ha senso usare Interlocked.Exchange è quel tipo di scenari? – Mike

+10

@ Mike: Quando si tratta di ciò che si può osservare nelle situazioni multithreading a bassa chiusura, sono ignorante come il prossimo. La risposta probabilmente varierà da processore a processore. Dovresti indirizzare la tua domanda a un esperto o leggere sull'argomento se ti interessa. Il libro di Joe Duffy e il suo blog sono buoni punti di partenza.La mia regola: non usare il multithreading. Se è necessario, utilizzare strutture di dati immutabili. Se non puoi, usa le serrature. Solo quando * devi * avere dati mutabili senza blocchi, dovresti considerare le tecniche di blocco basso. –

9

O il tuo collega si sbaglia, o sa qualcosa che le specifiche del linguaggio C# non lo fanno.

5.5 Atomicity of variable references:

"Legge e scrive dei seguenti tipi di dati sono atomiche: bool, char, byte, sbyte, breve, ushort, uint, int, float e tipi di riferimento."

Quindi, è possibile scrivere sul riferimento volatile senza il rischio di ottenere un valore danneggiato.

Si dovrebbe ovviamente essere attenti a come si decide quale thread dovrebbe recuperare i nuovi dati, per ridurre al minimo il rischio che più di un thread alla volta lo fa.

+1

Vero finchè la memoria è allineata correttamente. – zebrabox

+2

@guffa: sì, ho letto anche questo. questo lascia la domanda originale "l'assegnazione di riferimento è atomica, quindi perché è necessario Interlocked.Exchange (ref Object, Object)?" senza risposta –

+0

@zebrabox: cosa intendi? quando non lo sono? Cosa faresti? –

6

Interlocked.Exchange< T >

Imposta una variabile di tipo T specificato un valore specificato e restituisce il valore originale, come un'operazione atomica.

Cambia e restituisce il valore originale, è inutile perché si desidera solo modificarlo e, come ha detto Guffa, è già atomico.

A meno che un profiler come dimostrato di essere un collo di bottiglia nella vostra applicazione, si dovrebbe prendere in considerazione le serrature Conuna, è più facile da capire e dimostrare che il codice è giusto.

2

Iterlocked.Exchange() non è solo atomico, si occupa anche di visibilità memoria:

Le seguenti funzioni di sincronizzazione utilizzano le barriere appropriate per garantire memoria ordinazione:

funzioni che entrano o lasciano sezioni critiche

Funzioni che segnalano gli oggetti di sincronizzazione

Funzioni di attesa

funzioni interbloccate

Synchronization and Multiprocessor Issues

Ciò significa che, oltre alla atomicità assicura che:

  • Per il thread definendola:
    • No riordino delle istruzioni è fatto (dal compilatore, dal tempo di esecuzione o dall'hardware).
  • Per tutte le discussioni:
    • Non si legge nella memoria che accada prima di questa istruzione vedrà il cambiamento questa istruzione ha fatto.
    • Tutte le letture successive a questa istruzione vedranno la modifica apportata da questa istruzione.
    • Tutte le scritture in memoria dopo questa istruzione si verificheranno dopo che questa modifica delle istruzioni ha raggiunto la memoria principale (eseguendo il lavaggio di questa istruzione nella memoria principale al termine dell'operazione e non lasciando che l'hardware fluisca sul proprio tempo di attivazione).