Salta ai contenuti

Strutture Dati Concorrenti

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.


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.

TipoVersione Non Thread-SafeVersione Thread-Safe
Lista genericaList<T>ConcurrentBag<T>
Lista non genericaArrayListConcurrentBag<object>
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);
}
}
}
}
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);
}
}
}

Confronto Prestazioni:

OperazioneList<T> + lockConcurrentBag<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)

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.

TipoVersione Non Thread-SafeVersione Thread-Safe
Coda genericaQueue<T>ConcurrentQueue<T>
Coda bloccanteN/ABlockingCollection<T>
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();
}
}
}
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();
}
}
}

Confronto Prestazioni:

AspettoQueue<T> + lockConcurrentQueue<T>
Throughput (1 thread)✅ Ottimo✅ Buono
Throughput (8+ thread)🐌 Si degrada✅ Scala linearmente
Latency per operazione💚 Bassa💚 Bassa
ImplementazioneLock-basedLock-free (CAS)
Allocazioni memory💚 Minime💛 Moderate

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.

TipoVersione Non Thread-SafeVersione Thread-Safe
Stack genericaStack<T>ConcurrentStack<T>
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();
}
}
}
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();
}
}
}

Confronto Prestazioni:

AspettoStack<T> + lockConcurrentStack<T>
Push singolo✅ Veloce✅ Veloce
Push/Pop (alta contesa)🐌 Serializzato✅ Scalabile
PushRange/PopRange❌ Non disponibile✅ Ottimizzato
Memory overhead💚 Array-based💛 Linked list (overhead pointer)

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.

TipoVersione Non Thread-SafeVersione Thread-Safe
Dizionario genericoDictionary<TKey, TValue>ConcurrentDictionary<TKey, TValue>
HashtableHashtableConcurrentDictionary<object, object>

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();
}
}
}

ConcurrentDictionary<TKey, TValue> offre metodi atomici che combinano più operazioni:

// AddOrUpdate: Aggiunge o aggiorna atomicamente
dizionario.AddOrUpdate(
key: 42,
addValue: "Nuovo",
updateValueFactory: (key, oldValue) => oldValue + " Aggiornato"
);
// GetOrAdd: Ottiene o, se non esiste, aggiunge
string valore = dizionario.GetOrAdd(42, k => $"Valore per chiave {k}");
// TryUpdate: Aggiorna solo se il valore corrente corrisponde
bool aggiornato = dizionario.TryUpdate(
key: 42,
newValue: "Nuovo Valore",
comparisonValue: "Valore Atteso"
);

Confronto Prestazioni:

OperazioneDictionary + RWLockConcurrentDictionary
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”

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.

// Default: usa ConcurrentQueue<T> (FIFO)
var collection = new BlockingCollection<int>();
// Esplicito ConcurrentQueue
var collectionFIFO = new BlockingCollection<int>(new ConcurrentQueue<int>());
// ConcurrentStack per LIFO
var collectionLIFO = new BlockingCollection<int>(new ConcurrentStack<int>());
// Con capacità limitata (bounded)
var collectionBounded = new BlockingCollection<int>(boundedCapacity: 50);
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!");
}
}

6. Tabella Riassuntiva: Scelta della Struttura Dati

Sezione intitolata “6. Tabella Riassuntiva: Scelta della Struttura Dati”
ScenarioStruttura ConsigliataAlternativa
Lista senza ordine specificoConcurrentBag<T>ConcurrentQueue<T>
Coda FIFO standardConcurrentQueue<T>BlockingCollection<T>
Producer-Consumer con blockingBlockingCollection<T>ConcurrentQueue<T> + semafori
Stack LIFOConcurrentStack<T>N/A
Dizionario read/write mixConcurrentDictionary<TKey,TValue>Dictionary<K,V> + RWLock
Dizionario 99% readDictionary<K,V> (immutabile)ImmutableDictionary<K,V>
Set (valori univoci)ConcurrentDictionary<T,byte>HashSet<T> + lock