Per garantire la thread-safety nell’accesso a collezioni di oggetti in contesti multi-thread, fino ad oggi siamo stati abituati ad utilizzare i più svariati meccanismi di lock, magari prevedendo diversi livelli di granularità.
In questo ambito il framework 4.0 fornisce il namespace System.Collections.Concurrent che offre un set di classi thread-safe che si possono usare al posto di quelle di System.Collections e System.Collections.Generic.
Anzitutto, salutiamo con una certa soddisfazione la classe ConcurrentDictionary<TKey, TValue>, versione thread-safe della classe Dictionary.
Vengono forniti poi tre oggetti che implementano IProducerConsumerCollection<T>, un’interfaccia che definisce due metodi TryAdd e TryTake per scenari “producer/consumer”, ovvero scenari in cui più thread aggiungono o rimuovono elementi dalla stessa collection in modo concorrente. Gli oggetti in questione sono:
- ConcurrentBag<T>: implementazione thread-safe di una collezione non ordinata di elementi (eventualmente duplicabili). Questo oggetto è particolarmente ottimizzato per scenari in cui lo stesso thread produce e consuma gli elementi del bag.
- ConcurrentQueue<T>: versione concorrente di una coda FIFO. A differenza di quanto si sente dire spesso erroneamente, questa classe non costituisce un semplice wrapper della classe Queue ( tipo Queue.Synchronized() ), bensì qualcosa di più potente e performante che internamente fa uso di meccanismi di sincronizzazione e validazione più ottimizzati basati sulle classi SpinWait e Interlocked.
- ConcurrentStack<T>: versione concorrente di uno stack LIFO.
Infine abbiamo l’oggetto BlockingCollection<T>, che di fatto costituisce una classe di più alto livello. Una BlockingCollection è a tutti gli effetti un wrapper di un oggetto che implementa l’interfaccia IProducerConsumerCollection<T> (come i sopra citati ConcurrentBag<T>, ConcurrentQueue<T> e ConcurrentStack<T>). Ciò significa che di suo non implementa un meccanismo di storage interno, bensì introduce soltanto la logica di gestione thread-safe per scenari producer/consumer. Infatti, se non forniamo al costruttore un IProducerConsumerCollection, per default viene istanziata internamente una ConcurrentQueue (FIFO). Poiché la BlockingCollection è utile come buffer di scambio condiviso tra più thread, essa fornisce gratis anche dei meccanismi di blocco (metodi Add() e Take()) grazie ai quali possiamo fare in modo che i thread “producer” si blocchino se la collezione è piena (aspettando quindi che almeno un elemento venga rimosso dalla collezione) oppure viceversa che i thread “consumer” si blocchino quando cercano di rimuovere un elemento dalla collection interna vuota.