.NET - Blocco dizionario vs. ConcurrentDictionary


131

Non sono riuscito a trovare abbastanza informazioni sui ConcurrentDictionarytipi, quindi ho pensato di chiedere qui.

Attualmente, utilizzo a Dictionaryper contenere tutti gli utenti a cui si accede costantemente da più thread (da un pool di thread, quindi nessuna quantità esatta di thread) e ha accesso sincronizzato.

Recentemente ho scoperto che c'era una serie di raccolte thread-safe in .NET 4.0 e sembra essere molto piacevole. Mi chiedevo quale sarebbe stata l'opzione "più efficiente e più facile da gestire", dato che ho la possibilità di avere una normale Dictionarycon accesso sincronizzato o una ConcurrentDictionaryche è già sicura per i thread.

Riferimento a .NET 4.0 ConcurrentDictionary


Risposte:


146

Una raccolta thread-safe rispetto a una raccolta non thread-safe può essere considerata in modo diverso.

Prendi in considerazione un negozio senza commesso, tranne alla cassa. Hai un sacco di problemi se le persone non agiscono in modo responsabile. Ad esempio, supponiamo che un cliente prenda una lattina da una piramide mentre un impiegato sta costruendo la piramide, si scatenerà l'inferno. Oppure cosa succede se due clienti raggiungono contemporaneamente lo stesso oggetto, chi vince? Ci sarà una rissa? Questa è una raccolta non thread-safe. Esistono molti modi per evitare problemi, ma tutti richiedono un qualche tipo di blocco, o piuttosto un accesso esplicito in un modo o nell'altro.

D'altra parte, considera un negozio con un impiegato alla scrivania e puoi solo fare acquisti attraverso di lui. Ti metti in fila e gli chiedi un oggetto, te lo riporta e tu esci dalla fila. Se hai bisogno di più articoli, puoi solo raccogliere tutti gli articoli su ogni roundtrip che ricordi, ma devi fare attenzione a evitare di fare il commesso, questo farà arrabbiare gli altri clienti in fila dietro di te.

Ora considera questo. Nel negozio con un impiegato, cosa succede se arrivi fino in fondo alla fila e chiedi all'impiegato "Hai della carta igienica", e lui dice "Sì", e poi vai "Ok, io ' Ti risponderò quando saprò di cosa ho bisogno ", poi quando sarai di nuovo in prima fila, il negozio potrà ovviamente essere esaurito. Questo scenario non è impedito da una raccolta thread-safe.

Una raccolta thread-safe garantisce che le sue strutture di dati interne siano sempre valide, anche se vi si accede da più thread.

Una collezione non thread-safe non viene fornita con tali garanzie. Ad esempio, se aggiungi qualcosa a un albero binario su un thread, mentre un altro thread è impegnato a riequilibrare l'albero, non vi è alcuna garanzia che l'elemento verrà aggiunto, o anche che l'albero sia ancora valido in seguito, potrebbe essere corrotto oltre ogni speranza.

Una raccolta thread-safe, tuttavia, non garantisce che le operazioni sequenziali sul thread funzionino tutte sullo stesso "snapshot" della sua struttura di dati interna, il che significa che se si dispone di codice come questo:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

potresti ottenere una NullReferenceException perché tra tree.Counte tree.First(), un altro thread ha cancellato i nodi rimanenti nella struttura, il che significa First()che tornerà null.

Per questo scenario, è necessario vedere se la raccolta in questione ha un modo sicuro per ottenere ciò che si desidera, forse è necessario riscrivere il codice sopra o potrebbe essere necessario bloccare.


9
Ogni volta che leggo questo articolo, anche se è tutto vero, in qualche modo stiamo andando sulla strada sbagliata.
scope_creep

19
Il problema principale in un'applicazione abilitata per i thread è la mutevole struttura dei dati. Se riesci a evitarlo, ti risparmierai un sacco di problemi.
Lasse V. Karlsen,

89
Questa è una bella spiegazione della sicurezza del thread, tuttavia non posso fare a meno di pensare che in realtà non abbia affrontato la domanda del PO. Ciò che l'OP ha chiesto (e ciò che in seguito ho trovato questa domanda alla ricerca) è stata la differenza tra l'utilizzo di uno standard Dictionarye la gestione del blocco da parte dell'utente rispetto al ConcurrentDictionarytipo incorporato in .NET 4+. In realtà sono un po 'sconcertato dal fatto che questo sia stato accettato.
mclark1129,

4
La sicurezza del thread è molto più del semplice utilizzo della raccolta giusta. Usare la collezione giusta è un inizio, non l'unica cosa che devi affrontare. Immagino sia quello che l'OP voleva sapere. Non riesco a immaginare perché questo sia stato accettato, è passato un po 'di tempo da quando l'ho scritto.
Lasse V. Karlsen,

2
Penso che @ LasseV.Karlsen stia cercando di dire, è che ... la sicurezza dei thread è facile da raggiungere, ma è necessario fornire un livello di concorrenza nel modo in cui gestirli. Chiediti questo, "Posso fare più operazioni contemporaneamente mantenendo l'oggetto thread-safe?" Considera questo esempio: due clienti desiderano restituire articoli diversi. Quando tu, come manager, fai il viaggio per andare a rifornire il tuo negozio, non puoi semplicemente rifornire entrambi gli articoli in un viaggio e gestire comunque le richieste di entrambi i clienti, o farai un viaggio ogni volta che un cliente restituisce un articolo?
Alexandru,

68

Devi ancora fare molta attenzione quando usi le raccolte thread-safe perché thread-safe non significa che puoi ignorare tutti i problemi di threading. Quando una raccolta si pubblicizza come thread-safe, di solito significa che rimane in uno stato coerente anche quando più thread leggono e scrivono contemporaneamente. Ma ciò non significa che un singolo thread vedrà una sequenza "logica" di risultati se chiama più metodi.

Ad esempio, se si controlla prima se esiste una chiave e successivamente si ottiene il valore corrispondente alla chiave, quella chiave potrebbe non esistere più anche con una versione ConcurrentDictionary (perché un altro thread avrebbe potuto rimuovere la chiave). In questo caso è comunque necessario utilizzare il blocco (o meglio: combinare le due chiamate utilizzando TryGetValue ).

Quindi usali, ma non pensare che ti dia un passaggio gratuito per ignorare tutti i problemi di concorrenza. Devi ancora stare attento.


4
Quindi in pratica stai dicendo che se non usi correttamente l'oggetto non funzionerà?
ChaosPandion,

3
Fondamentalmente è necessario utilizzare TryAdd invece del comune Contains -> Aggiungi.
ChaosPandion,

3
@Bruno: No. Se non hai bloccato, un altro thread potrebbe aver rimosso la chiave tra le due chiamate.
Mark Byers,

13
Non intendo sembrare brusco qui, ma TryGetValueha sempre fatto parte Dictionaryed è sempre stato l'approccio raccomandato. Gli importanti metodi "concorrenti" che ConcurrentDictionaryintroduce sono AddOrUpdatee GetOrAdd. Quindi, buona risposta, ma avrebbe potuto scegliere un esempio migliore.
Aaronaught il

4
Giusto - a differenza del database che ha transazioni, la maggior parte della struttura di ricerca in memoria simultanea manca del concetto di isolamento , garantiscono solo che la struttura interna dei dati sia corretta.
Chang,

39

Internally ConcurrentDictionary utilizza un blocco separato per ogni bucket hash. Fintanto che utilizzi solo Add / TryGetValue e metodi simili che funzionano su singole voci, il dizionario funzionerà come una struttura di dati quasi priva di blocchi con il rispettivo vantaggio di prestazioni. OTOH i metodi di enumerazione (inclusa la proprietà Count) bloccano tutti i bucket contemporaneamente e sono quindi peggiori di un dizionario sincronizzato, dal punto di vista delle prestazioni.

Direi, basta usare ConcurrentDictionary.


15

Penso che il metodo ConcurrentDictionary.GetOrAdd sia esattamente ciò di cui hanno bisogno la maggior parte degli scenari multi-thread.


1
Vorrei anche usare TryGet, altrimenti perderai lo sforzo di inizializzare il secondo parametro su GetOrAdd ogni volta che la chiave esiste già.
Jorrit Schippers,

7
GetOrAdd ha il sovraccarico GetOrAdd (TKey, Func <TKey, TValue>) , quindi il secondo parametro può essere inizializzato in modo pigro solo nel caso in cui la chiave non esista nel dizionario. Inoltre TryGetValue viene utilizzato per recuperare i dati, non per modificarli. Le chiamate successive a ciascuno di questi metodi possono far sì che un altro thread possa aver aggiunto o rimosso la chiave tra le due chiamate.
sgnsajgon,

Questa dovrebbe essere la risposta marcata alla domanda, anche se il post di Lasse è eccellente.
Spivonious

13

Hai visto le estensioni reattive per .Net 3.5sp1. Secondo Jon Skeet, hanno eseguito il backport di un pacchetto di estensioni parallele e strutture di dati simultanee per .Net3.5 sp1.

Esiste una serie di esempi per .Net 4 Beta 2, che descrive in dettaglio abbastanza bene su come utilizzarli le estensioni parallele.

Ho appena trascorso l'ultima settimana a testare ConcurrentDictionary usando 32 thread per eseguire l'I / O. Sembra funzionare come pubblicizzato, il che indicherebbe che è stata messa in atto un'enorme quantità di test.

Modifica : .NET 4 ConcurrentDictionary e modelli.

Microsoft ha rilasciato un pdf chiamato Patterns of Paralell Programming. Vale davvero la pena di scaricarlo, poiché ha descritto in modo davvero dettagliato i modelli giusti da utilizzare per .Net 4 estensioni simultanee e gli schemi anti da evitare. Ecco qui.


4

Fondamentalmente vuoi andare con il nuovo ConcurrentDictionary. Immediatamente devi scrivere meno codice per rendere i programmi thread-safe.


c'è qualche problema se vado avanti con il Concurrent Dictionery?
kbvishnu,

1

È ConcurrentDictionaryun'ottima opzione se soddisfa tutte le esigenze di sicurezza del thread. In caso contrario, in altre parole stai facendo qualcosa di leggermente complesso, un normale Dictionary+ lockpotrebbe essere un'opzione migliore. Ad esempio, supponiamo che si stiano aggiungendo alcuni ordini in un dizionario e che si desideri mantenere aggiornato l'importo totale degli ordini. Puoi scrivere codice in questo modo:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Questo codice non è thread-safe. Più thread che aggiornano il _totalAmountcampo possono lasciarlo in uno stato danneggiato. Quindi potresti provare a proteggerlo con un lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Questo codice è "più sicuro", ma non è ancora sicuro per i thread. Non vi è alcuna garanzia che _totalAmountsia coerente con le voci nel dizionario. Un altro thread potrebbe tentare di leggere questi valori, per aggiornare un elemento dell'interfaccia utente:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

Il totalAmountpotrebbe non corrispondere al conteggio degli ordini nel dizionario. Le statistiche visualizzate potrebbero essere errate. A questo punto ti renderai conto che devi estendere la lockprotezione per includere l'aggiornamento del dizionario:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Questo codice è perfettamente sicuro, ma tutti i vantaggi dell'uso di a ConcurrentDictionarysono spariti.

  1. Le prestazioni sono peggiorate rispetto all'utilizzo di un normale Dictionary, perché il blocco interno all'interno ConcurrentDictionaryè ora dispendioso e ridondante.
  2. È necessario rivedere tutto il codice per gli usi non protetti delle variabili condivise.
  3. Sei bloccato con l'utilizzo di un'API imbarazzante ( TryAdd?, AddOrUpdate?).

Quindi il mio consiglio è: inizia con un Dictionary+ locke mantieni l'opzione di aggiornamento in seguito ConcurrentDictionarycome ottimizzazione delle prestazioni, se questa opzione è effettivamente praticabile. In molti casi non lo sarà.


0

Abbiamo usato ConcurrentDictionary per la raccolta cache, che viene ripopolata ogni 1 ora e quindi letta da più thread client, simile alla soluzione per il thread di questo esempio è sicuro? domanda.

Abbiamo scoperto che cambiarlo in  ReadOnlyDictionary ha  migliorato le prestazioni generali.


ReadOnlyDictionary è solo il wrapper attorno a un altro IDictionary. Cosa usi sotto il cofano?
Lu55,

@ Lu55, stavamo usando la libreria MS .Net e abbiamo scelto tra i dizionari disponibili / applicabili. Non sono a conoscenza dell'implementazione interna da parte di MS di ReadOnlyDictionary
Michael Freidgeim,
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.