Strutture Dati Concorrenti
Introduzione
Sezione intitolata “Introduzione”In ambienti multithreading, l’accesso concorrente alle strutture dati può causare race condition, corruzione dei dati e comportamenti non deterministici. .NET fornisce un insieme di collezioni thread-safe nel namespace System.Collections.Concurrent, progettate specificamente per scenari in cui più thread accedono simultaneamente alla stessa struttura dati.
Questa pagina presenta le principali strutture dati utilizzate in programmazione, confrontando le versioni standard (non thread-safe) con le loro controparti thread-safe, analizzando prestazioni e casi d’uso.
1. Liste e Collezioni Non Ordinate
Sezione intitolata “1. Liste e Collezioni Non Ordinate”1.1 Funzionalità Principale
Sezione intitolata “1.1 Funzionalità Principale”Le liste sono collezioni dinamiche che permettono l’aggiunta, la rimozione e l’accesso casuale agli elementi. In scenari concorrenti, quando l’ordine di inserimento non è rilevante, le “bag” (sacche) rappresentano un’alternativa più efficiente.
1.2 Versioni Disponibili
Sezione intitolata “1.2 Versioni Disponibili”| Tipo | Versione Non Thread-Safe | Versione Thread-Safe |
|---|---|---|
| Lista generica | List<T> | ConcurrentBag<T> |
| Lista non generica | ArrayList | ConcurrentBag<object> |
1.3 Esempi
Sezione intitolata “1.3 Esempi”1.3.1 Versione Non Thread-Safe con Lock
Sezione intitolata “1.3.1 Versione Non Thread-Safe con Lock”using System;using System.Collections.Generic;using System.Threading.Tasks;
class ListNonThreadSafe{ static List<int> lista = new(); static readonly object lockLista = new();
static void Main() { // Lancio 10 task che aggiungono elementi Task[] tasks = new Task[10]; for (int i = 0; i < 10; i++) { int taskId = i; tasks[i] = Task.Run(() => AggiungiElementi(taskId)); }
Task.WaitAll(tasks);
Console.WriteLine($"Elementi totali nella lista: {lista.Count}"); }
static void AggiungiElementi(int taskId) { for (int i = 0; i < 100; i++) { // LOCK NECESSARIO per evitare race condition lock (lockLista) { lista.Add(taskId * 100 + i); } } }}1.3.2 Versione Thread-Safe con ConcurrentBag
Sezione intitolata “1.3.2 Versione Thread-Safe con ConcurrentBag”using System;using System.Collections.Concurrent;using System.Threading.Tasks;
class ConcurrentBagExample{ static ConcurrentBag<int> bag = new();
static void Main() { // Lancio 10 task che aggiungono elementi Task[] tasks = new Task[10]; for (int i = 0; i < 10; i++) { int taskId = i; tasks[i] = Task.Run(() => AggiungiElementi(taskId)); }
Task.WaitAll(tasks);
Console.WriteLine($"Elementi totali nel bag: {bag.Count}");
// Consumo elementi in modo thread-safe while (bag.TryTake(out int valore)) { Console.WriteLine($"Estratto: {valore}"); } }
static void AggiungiElementi(int taskId) { for (int i = 0; i < 100; i++) { // NESSUN LOCK NECESSARIO - thread-safe internamente bag.Add(taskId * 100 + i); } }}1.4 Analisi e Confronto Prestazioni
Sezione intitolata “1.4 Analisi e Confronto Prestazioni”Confronto Prestazioni:
| Operazione | List<T> + lock | ConcurrentBag<T> |
|---|---|---|
| Add (bassa contesa) | ⚡ Molto veloce | ⚡ Molto veloce |
| Add (alta contesa) | 🐌 Rallentato da lock | ✅ Scalabile |
| Accesso per indice | ✅ O(1) | ❌ Non supportato |
| Iterazione ordinata | ✅ Mantiene ordine | ❌ Ordine non garantito |
| Memory overhead | 💚 Basso | 💛 Medio (strutture interne) |
2. Code FIFO (First-In-First-Out)
Sezione intitolata “2. Code FIFO (First-In-First-Out)”2.1 Funzionalità Principale
Sezione intitolata “2.1 Funzionalità Principale”Le code implementano il paradigma FIFO: il primo elemento inserito è il primo ad essere estratto. Sono fondamentali per implementare buffer, scheduling di task e pattern Producer-Consumer.
2.2 Versioni Disponibili
Sezione intitolata “2.2 Versioni Disponibili”| Tipo | Versione Non Thread-Safe | Versione Thread-Safe |
|---|---|---|
| Coda generica | Queue<T> | ConcurrentQueue<T> |
| Coda bloccante | N/A | BlockingCollection<T> |
2.3 Esempi
Sezione intitolata “2.3 Esempi”2.3.1 Versione Non Thread-Safe con Lock
Sezione intitolata “2.3.1 Versione Non Thread-Safe con Lock”using System;using System.Collections.Generic;using System.Threading.Tasks;
class QueueNonThreadSafe{ static Queue<string> coda = new(); static readonly object lockCoda = new();
static void Main() { // Producer task Task producer = Task.Run(() => Produce());
// Consumer task Task consumer = Task.Run(() => Consuma());
Task.WaitAll(producer, consumer); }
static void Produce() { for (int i = 0; i < 50; i++) { lock (lockCoda) { coda.Enqueue($"Messaggio {i}"); Console.WriteLine($"[Producer] Prodotto: Messaggio {i}"); } Task.Delay(50).Wait(); } }
static void Consuma() { int consumati = 0; while (consumati < 50) { lock (lockCoda) { if (coda.Count > 0) { string messaggio = coda.Dequeue(); Console.WriteLine($"[Consumer] Consumato: {messaggio}"); consumati++; } } Task.Delay(70).Wait(); } }}2.3.2 Versione Thread-Safe con ConcurrentQueue
Sezione intitolata “2.3.2 Versione Thread-Safe con ConcurrentQueue”using System;using System.Collections.Concurrent;using System.Threading.Tasks;
class ConcurrentQueueExample{ static ConcurrentQueue<string> coda = new();
static void Main() { // Producer task Task producer = Task.Run(() => Produce());
// Consumer task Task consumer = Task.Run(() => Consuma());
Task.WaitAll(producer, consumer); }
static void Produce() { for (int i = 0; i < 50; i++) { // NESSUN LOCK - thread-safe per design coda.Enqueue($"Messaggio {i}"); Console.WriteLine($"[Producer] Prodotto: Messaggio {i}"); Task.Delay(50).Wait(); } }
static void Consuma() { int consumati = 0; while (consumati < 50) { // TryDequeue è atomica e non bloccante if (coda.TryDequeue(out string messaggio)) { Console.WriteLine($"[Consumer] Consumato: {messaggio}"); consumati++; } Task.Delay(70).Wait(); } }}2.4 Analisi e Confronto Prestazioni
Sezione intitolata “2.4 Analisi e Confronto Prestazioni”Confronto Prestazioni:
| Aspetto | Queue<T> + lock | ConcurrentQueue<T> |
|---|---|---|
| Throughput (1 thread) | ✅ Ottimo | ✅ Buono |
| Throughput (8+ thread) | 🐌 Si degrada | ✅ Scala linearmente |
| Latency per operazione | 💚 Bassa | 💚 Bassa |
| Implementazione | Lock-based | Lock-free (CAS) |
| Allocazioni memory | 💚 Minime | 💛 Moderate |
3. Stack LIFO (Last-In-First-Out)
Sezione intitolata “3. Stack LIFO (Last-In-First-Out)”3.1 Funzionalità Principale
Sezione intitolata “3.1 Funzionalità Principale”Gli stack implementano il paradigma LIFO: l’ultimo elemento inserito è il primo ad essere estratto. Utilizzati per gestione di scope, undo/redo, e algoritmi ricorsivi.
3.2 Versioni Disponibili
Sezione intitolata “3.2 Versioni Disponibili”| Tipo | Versione Non Thread-Safe | Versione Thread-Safe |
|---|---|---|
| Stack generica | Stack<T> | ConcurrentStack<T> |
3.3 Esempi
Sezione intitolata “3.3 Esempi”3.3.1 Versione Non Thread-Safe con Lock
Sezione intitolata “3.3.1 Versione Non Thread-Safe con Lock”using System;using System.Collections.Generic;using System.Threading.Tasks;
class StackNonThreadSafe{ static Stack<int> stack = new(); static readonly object lockStack = new();
static void Main() { // Task che effettuano push Task[] pushers = new Task[5]; for (int i = 0; i < 5; i++) { int taskId = i; pushers[i] = Task.Run(() => PushElements(taskId)); }
Task.WaitAll(pushers);
// Task che effettuano pop Task[] poppers = new Task[5]; for (int i = 0; i < 5; i++) { poppers[i] = Task.Run(() => PopElements()); }
Task.WaitAll(poppers);
lock (lockStack) { Console.WriteLine($"Elementi rimanenti: {stack.Count}"); } }
static void PushElements(int taskId) { for (int i = 0; i < 20; i++) { lock (lockStack) { stack.Push(taskId * 100 + i); } } }
static void PopElements() { for (int i = 0; i < 20; i++) { lock (lockStack) { if (stack.Count > 0) { int value = stack.Pop(); Console.WriteLine($"Thread {Task.CurrentId}: Pop {value}"); } } Task.Delay(10).Wait(); } }}3.3.2 Versione Thread-Safe con ConcurrentStack
Sezione intitolata “3.3.2 Versione Thread-Safe con ConcurrentStack”using System;using System.Collections.Concurrent;using System.Threading.Tasks;
class ConcurrentStackExample{ static ConcurrentStack<int> stack = new();
static void Main() { // Task che effettuano push Task[] pushers = new Task[5]; for (int i = 0; i < 5; i++) { int taskId = i; pushers[i] = Task.Run(() => PushElements(taskId)); }
Task.WaitAll(pushers);
// Task che effettuano pop Task[] poppers = new Task[5]; for (int i = 0; i < 5; i++) { poppers[i] = Task.Run(() => PopElements()); }
Task.WaitAll(poppers);
Console.WriteLine($"Elementi rimanenti: {stack.Count}"); }
static void PushElements(int taskId) { for (int i = 0; i < 20; i++) { // NESSUN LOCK - operazione atomica stack.Push(taskId * 100 + i); } }
static void PopElements() { for (int i = 0; i < 20; i++) { // TryPop è atomica e safe if (stack.TryPop(out int value)) { Console.WriteLine($"Thread {Task.CurrentId}: Pop {value}"); } Task.Delay(10).Wait(); } }}3.4 Analisi e Confronto Prestazioni
Sezione intitolata “3.4 Analisi e Confronto Prestazioni”Confronto Prestazioni:
| Aspetto | Stack<T> + lock | ConcurrentStack<T> |
|---|---|---|
| Push singolo | ✅ Veloce | ✅ Veloce |
| Push/Pop (alta contesa) | 🐌 Serializzato | ✅ Scalabile |
| PushRange/PopRange | ❌ Non disponibile | ✅ Ottimizzato |
| Memory overhead | 💚 Array-based | 💛 Linked list (overhead pointer) |
4. Dizionari (Key-Value Maps)
Sezione intitolata “4. Dizionari (Key-Value Maps)”4.1 Funzionalità Principale
Sezione intitolata “4.1 Funzionalità Principale”I dizionari mappano chiavi univoche a valori, permettendo ricerca, inserimento e rimozione con complessità O(1) media. Sono la struttura dati più critica negli scenari concorrenti multi-lettura/scrittura.
4.2 Versioni Disponibili
Sezione intitolata “4.2 Versioni Disponibili”| Tipo | Versione Non Thread-Safe | Versione Thread-Safe |
|---|---|---|
| Dizionario generico | Dictionary<TKey, TValue> | ConcurrentDictionary<TKey, TValue> |
| Hashtable | Hashtable | ConcurrentDictionary<object, object> |
4.3 Esempi
Sezione intitolata “4.3 Esempi”4.3.1 Versione Non Thread-Safe con ReaderWriterLockSlim
Sezione intitolata “4.3.1 Versione Non Thread-Safe con ReaderWriterLockSlim”using System;using System.Collections.Generic;using System.Threading;using System.Threading.Tasks;
class DictionaryNonThreadSafe{ static Dictionary<int, string> dizionario = new(); static ReaderWriterLockSlim rwLock = new();
static void Main() { // Writer tasks Task[] writers = new Task[3]; for (int i = 0; i < 3; i++) { int writerId = i; writers[i] = Task.Run(() => ScriveChiavi(writerId)); }
// Reader tasks Task[] readers = new Task[10]; for (int i = 0; i < 10; i++) { readers[i] = Task.Run(() => LeggiChiavi()); }
Task.WaitAll(writers.Concat(readers).ToArray());
Console.WriteLine($"Chiavi totali: {dizionario.Count}"); }
static void ScriveChiavi(int writerId) { for (int i = 0; i < 100; i++) { int chiave = writerId * 1000 + i;
// WRITE LOCK necessario per modifiche rwLock.EnterWriteLock(); try { dizionario[chiave] = $"Valore-{writerId}-{i}"; } finally { rwLock.ExitWriteLock(); }
Task.Delay(5).Wait(); } }
static void LeggiChiavi() { Random rand = new(); for (int i = 0; i < 50; i++) { int chiave = rand.Next(0, 3000);
// READ LOCK permette letture concorrenti rwLock.EnterReadLock(); try { if (dizionario.ContainsKey(chiave)) { string valore = dizionario[chiave]; // Console.WriteLine($"Letto: {chiave} = {valore}"); } } finally { rwLock.ExitReadLock(); }
Task.Delay(2).Wait(); } }}4.3.2 Versione Thread-Safe con ConcurrentDictionary
Sezione intitolata “4.3.2 Versione Thread-Safe con ConcurrentDictionary”using System;using System.Collections.Concurrent;using System.Threading.Tasks;
class ConcurrentDictionaryExample{ static ConcurrentDictionary<int, string> dizionario = new();
static void Main() { // Writer tasks Task[] writers = new Task[3]; for (int i = 0; i < 3; i++) { int writerId = i; writers[i] = Task.Run(() => ScriveChiavi(writerId)); }
// Reader tasks Task[] readers = new Task[10]; for (int i = 0; i < 10; i++) { readers[i] = Task.Run(() => LeggiChiavi()); }
Task.WaitAll(writers.Concat(readers).ToArray());
Console.WriteLine($"Chiavi totali: {dizionario.Count}"); }
static void ScriveChiavi(int writerId) { for (int i = 0; i < 100; i++) { int chiave = writerId * 1000 + i;
// NESSUN LOCK - thread-safe internamente dizionario[chiave] = $"Valore-{writerId}-{i}";
// Oppure con TryAdd per evitare sovrascritture // dizionario.TryAdd(chiave, $"Valore-{writerId}-{i}");
Task.Delay(5).Wait(); } }
static void LeggiChiavi() { Random rand = new(); for (int i = 0; i < 50; i++) { int chiave = rand.Next(0, 3000);
// TryGetValue è atomica e thread-safe if (dizionario.TryGetValue(chiave, out string valore)) { // Console.WriteLine($"Letto: {chiave} = {valore}"); }
Task.Delay(2).Wait(); } }}4.4 Metodi Atomici Avanzati
Sezione intitolata “4.4 Metodi Atomici Avanzati”ConcurrentDictionary<TKey, TValue> offre metodi atomici che combinano più operazioni:
// AddOrUpdate: Aggiunge o aggiorna atomicamentedizionario.AddOrUpdate( key: 42, addValue: "Nuovo", updateValueFactory: (key, oldValue) => oldValue + " Aggiornato");
// GetOrAdd: Ottiene o, se non esiste, aggiungestring valore = dizionario.GetOrAdd(42, k => $"Valore per chiave {k}");
// TryUpdate: Aggiorna solo se il valore corrente corrispondebool aggiornato = dizionario.TryUpdate( key: 42, newValue: "Nuovo Valore", comparisonValue: "Valore Atteso");4.5 Analisi e Confronto Prestazioni
Sezione intitolata “4.5 Analisi e Confronto Prestazioni”Confronto Prestazioni:
| Operazione | Dictionary + RWLock | ConcurrentDictionary |
|---|---|---|
| Lettura (no contesa) | ⚡ Molto veloce | ✅ Veloce |
| Lettura (alta contesa) | ✅ Concorrente (RWLock) | ✅ Concorrente (fine-grained) |
| Scrittura (bassa contesa) | ✅ Veloce | ✅ Veloce |
| Scrittura (alta contesa) | 🐌 Serializzata | ✅ Scalabile (lock multipli) |
| AddOrUpdate atomico | ❌ Richiede 2+ lock | ✅ Singola operazione atomica |
| Memory footprint | 💚 Ottimizzato | 💛 +2-3x overhead |
Benchmark Indicativi (su 8-core, 1M operazioni):
Scenario: 80% Read, 20% Write (8 thread)- Dictionary + RWLock: ~450ms- ConcurrentDictionary: ~280ms (1.6x più veloce)
Scenario: 50% Read, 50% Write (8 thread)- Dictionary + RWLock: ~1200ms- ConcurrentDictionary: ~400ms (3x più veloce)5. BlockingCollection - Coordinazione Producer-Consumer
Sezione intitolata “5. BlockingCollection - Coordinazione Producer-Consumer”5.1 Funzionalità Principale
Sezione intitolata “5.1 Funzionalità Principale”BlockingCollection<T> è una wrapper thread-safe che aggiunge capacità di blocking a qualsiasi collezione che implementa IProducerConsumerCollection<T>. È la soluzione raccomandata per il pattern Producer-Consumer.
5.2 Caratteristiche Distintive
Sezione intitolata “5.2 Caratteristiche Distintive”5.3 Collezione Sottostante Personalizzabile
Sezione intitolata “5.3 Collezione Sottostante Personalizzabile”// Default: usa ConcurrentQueue<T> (FIFO)var collection = new BlockingCollection<int>();
// Esplicito ConcurrentQueuevar collectionFIFO = new BlockingCollection<int>(new ConcurrentQueue<int>());
// ConcurrentStack per LIFOvar collectionLIFO = new BlockingCollection<int>(new ConcurrentStack<int>());
// Con capacità limitata (bounded)var collectionBounded = new BlockingCollection<int>(boundedCapacity: 50);5.4 Esempio Completo: Pipeline di Elaborazione
Sezione intitolata “5.4 Esempio Completo: Pipeline di Elaborazione”using System;using System.Collections.Concurrent;using System.Threading;using System.Threading.Tasks;
class PipelineExample{ static BlockingCollection<int> buffer1 = new(boundedCapacity: 10); static BlockingCollection<string> buffer2 = new(boundedCapacity: 10);
static void Main() { // Stage 1: Producer - genera numeri Task producer = Task.Run(() => { for (int i = 1; i <= 100; i++) { buffer1.Add(i); Console.WriteLine($"[Producer] Generato: {i}"); Thread.Sleep(20); } buffer1.CompleteAdding(); // Segnala fine produzione Console.WriteLine("[Producer] Completato"); });
// Stage 2: Processor - trasforma numeri in stringhe Task processor = Task.Run(() => { // GetConsumingEnumerable blocca finché ci sono elementi o fino a CompleteAdding foreach (int numero in buffer1.GetConsumingEnumerable()) { string elaborato = $"Numero-{numero}-Elaborato"; buffer2.Add(elaborato); Console.WriteLine($"[Processor] Elaborato: {numero} -> {elaborato}"); Thread.Sleep(30); } buffer2.CompleteAdding(); Console.WriteLine("[Processor] Completato"); });
// Stage 3: Consumer - consuma stringhe elaborate Task consumer = Task.Run(() => { foreach (string item in buffer2.GetConsumingEnumerable()) { Console.WriteLine($"[Consumer] Ricevuto: {item}"); Thread.Sleep(50); } Console.WriteLine("[Consumer] Completato"); });
Task.WaitAll(producer, processor, consumer); Console.WriteLine("\nPipeline completata!"); }}5.5 Metodi Principali
Sezione intitolata “5.5 Metodi Principali”5.6 Analisi e Vantaggi
Sezione intitolata “5.6 Analisi e Vantaggi”6. Tabella Riassuntiva: Scelta della Struttura Dati
Sezione intitolata “6. Tabella Riassuntiva: Scelta della Struttura Dati”| Scenario | Struttura Consigliata | Alternativa |
|---|---|---|
| Lista senza ordine specifico | ConcurrentBag<T> | ConcurrentQueue<T> |
| Coda FIFO standard | ConcurrentQueue<T> | BlockingCollection<T> |
| Producer-Consumer con blocking | BlockingCollection<T> | ConcurrentQueue<T> + semafori |
| Stack LIFO | ConcurrentStack<T> | N/A |
| Dizionario read/write mix | ConcurrentDictionary<TKey,TValue> | Dictionary<K,V> + RWLock |
| Dizionario 99% read | Dictionary<K,V> (immutabile) | ImmutableDictionary<K,V> |
| Set (valori univoci) | ConcurrentDictionary<T,byte> | HashSet<T> + lock |