l'assegnazione dei riferimenti è atomica, quindi perché è necessario Interlocked.Exchange (ref Object, Object)?


108

Nel mio servizio web asmx multithread avevo un campo di classe _allData del mio tipo SystemData che consiste di pochi List<T>e Dictionary<T>contrassegnato come volatile. Il sistema data ( _allData) viene aggiornato di tanto in tanto e lo faccio creando un altro oggetto chiamato newDatae riempiendo le sue strutture dati con nuovi dati. Quando ha finito, mi limito ad assegnare

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 poiché l'assegnazione è atomica e i thread che hanno il riferimento ai vecchi dati continuano a usarlo e il resto ha i nuovi dati di sistema subito dopo l'assegnazione. Tuttavia il mio collega ha detto che invece di usare la volatileparola chiave e l'assegnazione semplice dovrei usare InterLocked.Exchangeperché ha detto che su alcune piattaforme non è garantito che l'assegnazione dei riferimenti sia atomica. Inoltre: quando dichiaro the _allDatacampo come volatileil

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

produce un avviso "un riferimento a un campo volatile non verrà trattato come volatile" Cosa dovrei pensare al riguardo?

Risposte:


180

Ci sono numerose domande qui. Considerandoli uno alla volta:

l'assegnazione dei riferimenti è atomica, quindi perché è necessario Interlocked.Exchange (ref Object, Object)?

L'assegnazione dei riferimenti è atomica. Interlocked.Exchange non fa solo l'assegnazione dei riferimenti. Effettua una lettura del valore corrente di una variabile, nasconde il vecchio valore e assegna il nuovo valore alla variabile, il tutto come un'operazione atomica.

il mio collega ha detto che su alcune piattaforme non è garantito che l'assegnazione dei riferimenti sia atomica. Il mio collega aveva ragione?

No. L'assegnazione dei riferimenti è garantita come atomica su tutte le piattaforme .NET.

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

Non necessariamente. Il tuo collega potrebbe darti buoni consigli per cattivi motivi. Forse c'è qualche altro motivo per cui dovresti usare Interlocked.Exchange. La programmazione senza blocchi è incredibilmente difficile e nel momento in cui ti allontani da pratiche consolidate sposate da esperti del settore, sei tra le erbacce e rischia 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 un avviso "un riferimento a un campo volatile non verrà trattato come volatile" Cosa dovrei pensare al riguardo?

Dovresti capire perché questo è un problema in generale. Ciò porterà alla comprensione del motivo per cui 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 memorizzi nella cache i valori di questo campo e assicurati che qualsiasi lettura o scrittura di questo campo non viene "spostato avanti e indietro nel tempo" tramite incongruenze della cache del processore. "

(Presumo che tu abbia già capito tutto questo. Se non hai una comprensione dettagliata del significato di volatile e di come influisce sulla semantica della cache del processore, allora non capisci come funziona e non dovresti usare volatile. Programmi senza blocco sono molto difficili da mettere a punto; assicurati che il tuo programma sia giusto perché capisci come funziona, non giusto per caso.)

Supponiamo ora di creare una variabile che è un alias di un campo volatile passando un riferimento a quel campo. All'interno del metodo chiamato, il compilatore non ha alcun motivo 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. Questo può rovinare completamente la tua logica senza blocco; il presupposto è sempre che un campo volatile sia sempre accessibile con semantica volatile. Non ha senso trattarlo come volatile a volte e non altre volte; devi essere sempre coerente altrimenti non puoi garantire la coerenza su altri accessi.

Pertanto, il compilatore avvisa quando lo fai, perché probabilmente rovinerà completamente la tua logica senza blocchi sviluppata con cura.

Ovviamente, Interlocked.Exchange è scritto per aspettarsi un campo volatile e fare la cosa giusta. L'avvertimento è quindi fuorviante. Me ne rammarico molto; quello che avremmo dovuto fare è implementare un meccanismo in base al quale un autore di un metodo come Interlocked.Exchange potrebbe mettere un attributo sul metodo dicendo "questo metodo che richiede un ref impone la semantica volatile sulla variabile, quindi sopprimi l'avvertimento". Forse in una futura versione del compilatore lo faremo.


1
Da quello che ho sentito, Interlocked.Exchange garantisce anche la creazione di una barriera di memoria. Quindi, se ad esempio crei un nuovo oggetto, quindi assegni un paio di proprietà e quindi memorizzi l'oggetto in un altro riferimento senza utilizzare 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

12
@ Mike: Quando si tratta di ciò che è possibile osservare in situazioni multithread low-lock, sono ignorante come il prossimo ragazzo. La risposta probabilmente varierà da processore a processore. Dovresti rivolgere la tua domanda a un esperto o documentarti sull'argomento se ti interessa. Il libro di Joe Duffy e il suo blog sono ottimi punti di partenza. La mia regola: non usare il multithreading. Se è necessario, utilizzare strutture di dati immutabili. Se non puoi, usa i lucchetti. Solo quando è necessario disporre di dati modificabili senza blocchi è necessario considerare le tecniche di blocco basso.
Eric Lippert

Grazie per la tua risposta Eric. Mi interessa davvero, ecco perché ho letto libri e blog sulle strategie di multithreading e blocco e ho anche cercato di implementarle nel mio codice. Ma c'è ancora molto da imparare ...
Mike

2
@EricLippert Tra "non usare multithreading" e "se è necessario, usa strutture dati immutabili", inserisco il livello intermedio e molto comune di "fai in modo che un thread figlio usi solo oggetti di input di proprietà esclusiva e il thread padre consuma i risultati solo quando il bambino ha finito ". Come in var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
Giovanni

1
@ John: Questa è una buona idea. Cerco di trattare i thread come processi economici: sono lì per fare un lavoro e produrre un risultato, non per correre come un secondo thread di controllo all'interno delle strutture dati del programma principale. Ma se la quantità di lavoro che il thread sta facendo è così grande che è ragionevole trattarlo come un processo, allora dico solo di renderlo un processo!
Eric Lippert

9

O il tuo collega si sbaglia o sa qualcosa che la specifica del linguaggio C # non sa.

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 types."

Quindi, puoi scrivere nel riferimento volatile senza il rischio di ottenere un valore danneggiato.

Ovviamente dovresti stare attento a come decidi quale thread deve recuperare i nuovi dati, per ridurre al minimo il rischio che più di un thread alla volta lo faccia.


3
@guffa: si l'ho letto anche io. questo lascia la domanda originale "l'assegnazione dei riferimenti è atomica, quindi perché è necessario Interlocked.Exchange (ref Object, Object)?" senza risposta
char m

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

@matti: è necessario quando devi leggere e scrivere un valore come operazione atomica.
Guffa

Quante volte devi preoccuparti che la memoria non sia allineata correttamente in .NET? Cose pesanti per l'interoperabilità?
Skurmedel

1
@zebrabox: la specifica non elenca questo avvertimento, fornisce una dichiarazione molto chiara. Hai un riferimento per una situazione non allineata alla memoria in cui un riferimento di lettura o scrittura non riesce ad essere atomico? Sembra che ciò violerebbe il linguaggio molto chiaro nelle specifiche.
TJ Crowder

6

Interlocked.Exchange <T>

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

Cambia e restituisce il valore originale, è inutile perché vuoi solo cambiarlo e, come diceva Guffa, è già atomico.

A meno che un profiler non abbia dimostrato di essere un collo di bottiglia nella tua applicazione, dovresti considerare di sbloccare i blocchi, è più facile capire e dimostrare che il tuo codice è corretto.


3

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

Le seguenti funzioni di sincronizzazione utilizzano le barriere appropriate per garantire l'ordine della memoria:

Funzioni che entrano o escono da sezioni critiche

Funzioni che segnalano oggetti di sincronizzazione

Funzioni di attesa

Funzioni interbloccate

Problemi di sincronizzazione e multiprocessore

Ciò significa che oltre all'atomicità garantisce che:

  • Per il thread che lo chiama:
    • Non viene effettuato alcun riordino delle istruzioni (dal compilatore, dal runtime o dall'hardware).
  • Per tutti i thread:
    • Nessuna lettura in memoria prima di questa istruzione vedrà la modifica apportata da questa istruzione.
    • Tutte le letture dopo questa istruzione vedranno la modifica apportata da questa istruzione.
    • Tutte le scritture nella memoria dopo questa istruzione avverranno dopo che questa modifica dell'istruzione ha raggiunto la memoria principale (scaricando questa modifica dell'istruzione nella memoria principale quando è terminata e non lasciare che l'hardware lo scarichi sulla sua temporizzazione).
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.