Sostieni AppuntiFacili con una piccola donazione su PayPal
Dona con PayPalQuando un programma cresce, quasi sempre incontra almeno uno di questi problemi:
Qui entra in gioco il tema dei thread. Un thread è un’unità di esecuzione interna a un processo. Più thread dello stesso processo condividono gran parte delle risorse del processo stesso, ma mantengono un proprio flusso di esecuzione.
Un processo è un’istanza in esecuzione di un programma. Ha:
Un thread è invece il percorso effettivo di esecuzione del codice dentro un processo. Ogni thread ha:
La distinzione fondamentale è questa:
flowchart TB
subgraph OS["Sistema Operativo"]
subgraph P1["Processo A"]
H1["Heap condiviso"]
S1["Static / Global condivisi"]
T1["Thread A1\nStack privato"]
T2["Thread A2\nStack privato"]
T1 --- H1
T2 --- H1
T1 --- S1
T2 --- S1
end
subgraph P2["Processo B"]
H2["Heap isolato"]
S2["Static / Global isolati"]
T3["Thread B1\nStack privato"]
T3 --- H2
T3 --- S2
end
end
Conseguenze pratiche:
I due concetti sono collegati ma non identici.
Distinzione formale:
Un singolo core con time slicing può offrire concorrenza senza vero parallelismo: il sistema operativo alterna i thread così rapidamente da dare l’impressione di simultaneità.
Su macchina multicore puoi avere parallelismo reale.
flowchart LR
A["Concorrenza\npiù task in avanzamento"] --> B["Possibile anche su 1 core"]
C["Parallelismo\npiù task in esecuzione simultanea"] --> D["Richiede almeno 2 unità esecutive"]
Una formula utile per capire il limite teorico del parallelismo è la legge di Amdahl:
dove:
Se solo il 70% del codice è parallelizzabile, anche con infiniti core il guadagno massimo resta:
Quindi i thread non sono una bacchetta magica: servono quando il problema è davvero concorrente o parallelizzabile.
ThreadLa classe System.Threading.Thread rappresenta un thread gestito esplicito. Oggi, per molta programmazione applicativa moderna, si preferiscono Task, async/await e il thread pool; tuttavia Thread resta importante per capire i fondamenti e per alcuni scenari specifici.
Il costruttore di Thread accetta tipicamente:
ThreadStart, se il metodo non ha parametri;ParameterizedThreadStart, se il metodo riceve un parametro object?.using System;
using System.Threading;
public class Program
{
public static void Lavoro()
{
Console.WriteLine($"Eseguito da: {Thread.CurrentThread.ManagedThreadId}");
}
public static void Main()
{
Thread thread = new Thread(Lavoro);
thread.Start();
thread.Join();
}
}
Start() mette il thread nello scheduler del runtime/OS. Non significa “inizia esattamente adesso”, ma “è pronto a essere eseguito”.
Start, Join, Sleep, AbortThread.Start()Avvia il thread una sola volta. Un thread terminato non può essere riavviato.
Thread t = new Thread(() => Console.WriteLine("Partito"));
t.Start();
Thread.Join()Blocca il thread chiamante finché il thread target non termina.
Thread worker = new Thread(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Lavoro completato");
});
worker.Start();
worker.Join();
Console.WriteLine("Continuo solo dopo la fine del worker");
Esistono anche overload con timeout:
bool terminato = worker.Join(500);
Console.WriteLine($"Terminato entro 500ms? {terminato}");
Thread.Sleep()Sospende il thread corrente per almeno il tempo specificato. Il thread non esegue lavoro utile durante la pausa.
Thread.Sleep(2000);
Sleep non è uno strumento di sincronizzazione preciso. È spesso un anti-pattern quando viene usato per “aspettare che l’altro thread finisca”. In quei casi è meglio usare Join, Monitor, SemaphoreSlim, Task, eventi di sincronizzazione o strutture concorrenti.
Thread.Abort()Storicamente tentava di interrompere forzatamente un thread lanciando un’eccezione asincrona nel thread target. Oggi è considerato deprecato e pericoloso:
In pratica:
CancellationToken, timeout o primitive di sincronizzazione.Thread.IsBackgroundDetermina se il thread è foreground o background.
Thread t = new Thread(Lavoro);
t.IsBackground = true;
t.Start();
Thread.IsAliveIndica se il thread è stato avviato e non è ancora terminato.
Console.WriteLine(t.IsAlive);
Thread.NameAssegna un nome descrittivo utile per debugging e logging.
Thread t = new Thread(Lavoro);
t.Name = "ImportWorker";
t.Start();
La differenza è cruciale:
Se tutti i thread foreground finiscono, il processo termina anche se restano thread background ancora in esecuzione.
using System;
using System.Threading;
public class Program
{
public static void Main()
{
Thread bg = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Background step {i}");
Thread.Sleep(500);
}
});
bg.IsBackground = true;
bg.Start();
Console.WriteLine("Main termina subito");
}
}
Questo programma può chiudersi prima che il thread stampi tutti i messaggi, perché il thread è background.
ThreadStart e ParameterizedThreadStartThreadStart rappresenta un metodo senza parametri:
ThreadStart start = () => Console.WriteLine("Senza parametri");
Thread t = new Thread(start);
ParameterizedThreadStart rappresenta un metodo con un singolo parametro object?:
void Stampa(object? value)
{
Console.WriteLine($"Valore: {value}");
}
Thread t = new Thread(new ParameterizedThreadStart(Stampa));
t.Start(42);
Il limite è evidente:
object?;Per questo spesso è preferibile catturare i parametri con una lambda:
string file = "report.csv";
int retry = 3;
Thread t = new Thread(() => Processa(file, retry));
t.Start();
Hai tre strategie principali:
ParameterizedThreadStartEsempio con oggetto dedicato:
using System;
using System.Threading;
public class JobData
{
public string FileName { get; set; } = "";
public int Priority { get; set; }
}
public class Program
{
public static void Main()
{
JobData job = new JobData
{
FileName = "report.csv",
Priority = 2
};
Thread thread = new Thread(() =>
{
Console.WriteLine($"File: {job.FileName}, Priority: {job.Priority}");
});
thread.Start();
thread.Join();
}
}
Questa soluzione è più leggibile e type-safe.
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Runnable : Start()
Runnable --> Running : scheduler
Running --> WaitSleepJoin : Sleep / Wait / Join
WaitSleepJoin --> Runnable : timeout / signal
Running --> Stopped : fine metodo / eccezione non gestita
Nota importante: gli stati precisi esposti dall’API (ThreadState) sono storici e non sempre ideali per ragionare su sistemi moderni. Il diagramma qui sopra è più utile come modello mentale operativo.
Creare thread manualmente è costoso. Per questo .NET mette a disposizione il ThreadPool, un insieme di worker thread riutilizzabili gestiti dal runtime.
ThreadPoolIl thread pool:
Task.Quando hai lavori brevi, indipendenti e numerosi, il pool è quasi sempre migliore del creare thread manualmente.
ThreadPool.QueueUserWorkItemIl modo classico per accodare lavoro al pool è QueueUserWorkItem.
using System;
using System.Threading;
public class Program
{
public static void Main()
{
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine($"Worker pool: {Thread.CurrentThread.ManagedThreadId}");
});
Thread.Sleep(500);
}
}
Con stato:
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine($"Elaboro ordine {state}");
}, 12345);
Il thread pool è appropriato quando:
Non è ideale quando:
Sleep, I/O bloccante o lock molto lunghi.new Thread(...)Creare un thread comporta costi non banali:
Una stima utile è:
dove molti thread managed hanno stack iniziale dell’ordine del megabyte. Con 500 thread puoi facilmente arrivare a centinaia di MB di memoria riservata.
Anche il costo di context switch cresce:
dove dipende dall’hardware, dalla cache e dal carico del sistema. Se i thread sono troppi rispetto ai core, il tempo speso a cambiare contesto può diventare dominante.
Quindi:
SetMinThreads e SetMaxThreadsPuoi influenzare il comportamento del pool:
ThreadPool.GetMinThreads(out int minWorker, out int minIo);
ThreadPool.GetMaxThreads(out int maxWorker, out int maxIo);
Console.WriteLine($"Min worker: {minWorker}, Max worker: {maxWorker}");
E puoi impostare limiti:
ThreadPool.SetMinThreads(workerThreads: 8, completionPortThreads: 8);
ThreadPool.SetMaxThreads(workerThreads: 200, completionPortThreads: 200);
Attenzione:
Nella pratica si interviene solo dopo profiling serio.
Se stai scrivendo codice moderno:
Task.Run o API TPL;Thread manuale solo se hai davvero bisogno di un thread dedicato.Quando più thread accedono agli stessi dati, il problema principale non è “farli partire”, ma coordinarli correttamente.
Una race condition si verifica quando il risultato dipende dall’ordine o dall’interleaving temporale di operazioni concorrenti non sincronizzate.
Definizione formale semplificata:
Esempio classico con contatore:
using System;
using System.Threading;
public class Program
{
private static int _counter = 0;
public static void Main()
{
Thread t1 = new Thread(Incrementa);
Thread t2 = new Thread(Incrementa);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine(_counter);
}
private static void Incrementa()
{
for (int i = 0; i < 100_000; i++)
{
_counter++;
}
}
}
Potresti aspettarti 200000, ma il valore finale può essere inferiore.
Perché _counter++ non è atomico: concettualmente equivale a:
_counter;Interleaving possibile:
T1 legge 10
T2 legge 10
T1 scrive 11
T2 scrive 11
Un incremento viene perso.
lock e Monitor.Enter/ExitIn C#, lock è la forma più usata per proteggere una sezione critica, cioè la parte di codice che deve essere eseguita da un solo thread alla volta.
private static readonly object _sync = new();
private static int _counter = 0;
private static void Incrementa()
{
for (int i = 0; i < 100_000; i++)
{
lock (_sync)
{
_counter++;
}
}
}
lock è sintassi comoda sopra Monitor.Enter e Monitor.Exit:
bool lockTaken = false;
try
{
Monitor.Enter(_sync, ref lockTaken);
_counter++;
}
finally
{
if (lockTaken)
Monitor.Exit(_sync);
}
Regole importanti:
this;Una sezione critica deve:
Sleep, chiamate lente o callback verso codice esterno.Più la sezione critica è lunga, più aumentano:
Un deadlock è una situazione in cui un insieme di thread resta bloccato per sempre perché ognuno attende una risorsa detenuta da un altro.
Esempio classico:
using System;
using System.Threading;
public class Program
{
private static readonly object LockA = new();
private static readonly object LockB = new();
public static void Main()
{
Thread t1 = new Thread(() =>
{
lock (LockA)
{
Thread.Sleep(100);
lock (LockB)
{
Console.WriteLine("T1 completato");
}
}
});
Thread t2 = new Thread(() =>
{
lock (LockB)
{
Thread.Sleep(100);
lock (LockA)
{
Console.WriteLine("T2 completato");
}
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
sequenceDiagram
participant T1 as Thread 1
participant A as Lock A
participant T2 as Thread 2
participant B as Lock B
T1->>A: acquisisce A
T2->>B: acquisisce B
T1->>B: attende B
T2->>A: attende A
Note over T1,T2: attesa circolare => deadlock
Un deadlock può verificarsi se coesistono queste quattro condizioni:
Per prevenire deadlock, basta rompere almeno una di queste condizioni. La strategia pratica più comune è imporre un ordine globale di acquisizione dei lock.
Monitor.Wait, Pulse, PulseAllMonitor non serve solo per mutua esclusione. Permette anche di coordinare thread che devono aspettare una condizione logica.
Wait: rilascia temporaneamente il lock e sospende il thread;Pulse: risveglia un thread in attesa su quel monitor;PulseAll: risveglia tutti i thread in attesa.Esempio producer-consumer minimale con buffer singolo:
using System;
using System.Threading;
public class SingleSlotBuffer
{
private readonly object _sync = new();
private int _value;
private bool _hasValue;
public void Produce(int value)
{
lock (_sync)
{
while (_hasValue)
Monitor.Wait(_sync);
_value = value;
_hasValue = true;
Monitor.PulseAll(_sync);
}
}
public int Consume()
{
lock (_sync)
{
while (!_hasValue)
Monitor.Wait(_sync);
int result = _value;
_hasValue = false;
Monitor.PulseAll(_sync);
return result;
}
}
}
Osserva il pattern corretto:
Wait dentro un ciclo while, non if;Wait/Pulse deve essere lo stesso del lock.MutexMutex è una primitiva di mutua esclusione simile concettualmente a lock, ma con una differenza importante: può essere cross-process.
using System;
using System.Threading;
public class Program
{
private static readonly Mutex Mutex = new(false, "Global\\ImparareFacileMutex");
public static void Main()
{
Mutex.WaitOne();
try
{
Console.WriteLine("Sezione esclusiva anche tra processi");
}
finally
{
Mutex.ReleaseMutex();
}
}
}
Differenze principali:
lock/Monitor: leggero, intra-processo, molto usato;Mutex: più pesante, può sincronizzare anche processi diversi.Se non ti serve sincronizzazione tra processi, lock è in genere preferibile.
Semaphore e SemaphoreSlimUn semaforo non dà accesso esclusivo a uno solo, ma a un massimo di N thread contemporaneamente.
using System;
using System.Threading;
public class Program
{
private static readonly SemaphoreSlim Gate = new(3);
public static void Main()
{
for (int i = 0; i < 10; i++)
{
int jobId = i;
ThreadPool.QueueUserWorkItem(_ =>
{
Gate.Wait();
try
{
Console.WriteLine($"Job {jobId} entra");
Thread.Sleep(1000);
}
finally
{
Console.WriteLine($"Job {jobId} esce");
Gate.Release();
}
});
}
Thread.Sleep(5000);
}
}
Usi tipici:
Differenza:
Semaphore: più vicino alla primitiva OS, anche cross-process in certi scenari;SemaphoreSlim: più leggero, ottimizzato per uso intra-processo e molto comune in .NET moderno.ReaderWriterLockSlimQuando hai molte letture e poche scritture, un semplice lock può essere troppo restrittivo perché serializza tutto.
ReaderWriterLockSlim consente:
using System;
using System.Collections.Generic;
using System.Threading;
public class Cache
{
private readonly Dictionary<int, string> _data = new();
private readonly ReaderWriterLockSlim _rw = new();
public string? Get(int key)
{
_rw.EnterReadLock();
try
{
return _data.TryGetValue(key, out string? value) ? value : null;
}
finally
{
_rw.ExitReadLock();
}
}
public void Set(int key, string value)
{
_rw.EnterWriteLock();
try
{
_data[key] = value;
}
finally
{
_rw.ExitWriteLock();
}
}
}
È utile quando il carico è fortemente read-heavy. Va però usato con criterio: introduce complessità maggiore rispetto a un normale lock.
volatileNon sempre serve bloccare intere sezioni critiche. Per operazioni molto semplici esistono strumenti più leggeri.
InterlockedLa classe Interlocked offre operazioni atomiche su variabili condivise.
Operazioni comuni:
IncrementDecrementExchangeCompareExchangeInterlocked.Increment e Decrementusing System;
using System.Threading;
public class Program
{
private static int _counter;
public static void Main()
{
Thread t1 = new Thread(Work);
Thread t2 = new Thread(Work);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine(_counter);
}
private static void Work()
{
for (int i = 0; i < 100_000; i++)
{
Interlocked.Increment(ref _counter);
}
}
}
Qui il valore finale sarà corretto.
Interlocked.ExchangeSostituisce atomicamente il valore:
int stato = 0;
Interlocked.Exchange(ref stato, 1);
Interlocked.CompareExchangeÈ la base di molti algoritmi lock-free. Aggiorna il valore solo se coincide con un valore atteso.
int stato = 0;
int originale = Interlocked.CompareExchange(ref stato, 1, 0);
if (originale == 0)
{
Console.WriteLine("Transizione eseguita");
}
Questo significa: “porta stato a 1 solo se adesso è 0”.
volatileLa keyword volatile non rende un’operazione complessa atomica. Serve invece a imporre garanzie di visibilità tra thread.
using System;
using System.Threading;
public class Program
{
private static volatile bool _stopRequested = false;
public static void Main()
{
Thread worker = new Thread(() =>
{
while (!_stopRequested)
{
}
Console.WriteLine("Stop ricevuto");
});
worker.Start();
Thread.Sleep(500);
_stopRequested = true;
worker.Join();
}
}
volatile comunica al compilatore e al runtime che:
In breve:
x++.volatile e memory barrierQuando si parla di concorrenza, non basta sapere “chi esegue per primo”. Conta anche l’ordine con cui letture e scritture diventano visibili agli altri core.
Una semplificazione utile è:
Non è una soluzione universale. Nella maggior parte dei casi:
lock per invarianti composte;Interlocked per aggiornamenti atomici semplici;volatile solo quando hai davvero bisogno di una flag di visibilità o pattern molto specifici.Interlocked e lock| Aspetto | Interlocked | lock |
|---|---|---|
| Tipo di operazione | atomica singola | sezione critica arbitraria |
| Overhead | molto basso | più alto |
| Componibilità | limitata | alta |
| Protezione di invarianti multiple | no | sì |
| Rischio deadlock | no diretto | sì, se usato male |
Esempio:
Interlocked.Increment;lock.ThreadLocal<T>ThreadLocal<T> consente di mantenere dati locali a ogni thread, evitando condivisione accidentale.
Ogni thread vede la propria istanza del valore:
using System;
using System.Threading;
public class Program
{
private static readonly ThreadLocal<int> LocalCounter = new(() => 0);
public static void Main()
{
Thread t1 = new Thread(Work);
Thread t2 = new Thread(Work);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
private static void Work()
{
for (int i = 0; i < 3; i++)
{
LocalCounter.Value++;
}
Console.WriteLine(
$"Thread {Thread.CurrentThread.ManagedThreadId}: {LocalCounter.Value}");
}
}
Ogni thread stamperà il proprio valore senza interferire con gli altri.
Scenari tipici:
static e con i parametri| Strumento | Visibilità | Condivisione |
|---|---|---|
campo static | globale al processo | condiviso da tutti i thread |
| parametro di metodo | locale alla chiamata | esplicito, non condiviso se non passato |
ThreadLocal<T> | locale al thread | una copia per thread |
ThreadLocal<T> è utile quando:
Attenzione però: con thread pool e Task, il lavoro può spostarsi tra thread diversi. Quindi ThreadLocal<T> è perfetto per thread veri e propri o logiche thread-bound, ma non va confuso con il concetto di “contesto logico dell’operazione”.
Le collezioni standard come List<T> e Dictionary<TKey, TValue> non sono sicure per accesso concorrente in lettura/scrittura senza sincronizzazione esterna.
Per questo .NET offre collezioni concorrenti in System.Collections.Concurrent.
ConcurrentDictionaryPensato per accessi concorrenti a mappa chiave-valore.
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
public class Program
{
public static async Task Main()
{
ConcurrentDictionary<string, int> counts = new();
await Task.WhenAll(
Task.Run(() => counts.AddOrUpdate("ok", 1, (_, old) => old + 1)),
Task.Run(() => counts.AddOrUpdate("ok", 1, (_, old) => old + 1))
);
Console.WriteLine(counts["ok"]);
}
}
ConcurrentQueueCoda FIFO thread-safe.
using System;
using System.Collections.Concurrent;
ConcurrentQueue<int> queue = new();
queue.Enqueue(10);
queue.Enqueue(20);
if (queue.TryDequeue(out int value))
{
Console.WriteLine(value);
}
ConcurrentBagCollezione non ordinata ottimizzata per scenari con molti producer/consumer e accessi locali per thread.
using System;
using System.Collections.Concurrent;
ConcurrentBag<string> bag = new();
bag.Add("A");
bag.Add("B");
if (bag.TryTake(out string? item))
{
Console.WriteLine(item);
}
ConcurrentStackStack LIFO thread-safe.
using System;
using System.Collections.Concurrent;
ConcurrentStack<int> stack = new();
stack.Push(1);
stack.Push(2);
if (stack.TryPop(out int item))
{
Console.WriteLine(item);
}
BlockingCollection<T> per producer-consumerBlockingCollection<T> aggiunge semantica di blocco e completamento sopra una collezione concorrente sottostante.
using System;
using System.Collections.Concurrent;
using System.Threading;
public class Program
{
public static void Main()
{
using BlockingCollection<int> queue = new(new ConcurrentQueue<int>());
Thread producer = new Thread(() =>
{
for (int i = 1; i <= 5; i++)
{
queue.Add(i);
Console.WriteLine($"Prodotto: {i}");
}
queue.CompleteAdding();
});
Thread consumer = new Thread(() =>
{
foreach (int item in queue.GetConsumingEnumerable())
{
Console.WriteLine($"Consumato: {item}");
}
});
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
}
}
Punti forti:
Wait/Pulse;CompleteAdding().Una delle insidie classiche è pensare che un’eccezione lanciata dentro un thread si propaghi automaticamente al thread chiamante. Non funziona così.
Se un thread secondario lancia un’eccezione non gestita:
Start();Esempio:
using System;
using System.Threading;
public class Program
{
public static void Main()
{
Thread t = new Thread(() =>
{
throw new InvalidOperationException("Errore nel worker");
});
t.Start();
t.Join();
Console.WriteLine("Main non cattura automaticamente l'eccezione del worker");
}
}
Un try/catch attorno a t.Start() o t.Join() non basta per catturare quell’errore.
La strategia corretta è catturare l’eccezione dentro il thread e comunicarla in modo esplicito.
using System;
using System.Threading;
public class Program
{
private static Exception? _workerException;
public static void Main()
{
Thread t = new Thread(() =>
{
try
{
Esegui();
}
catch (Exception ex)
{
_workerException = ex;
}
});
t.Start();
t.Join();
if (_workerException is not null)
{
Console.WriteLine($"Errore catturato dal worker: {_workerException.Message}");
}
}
private static void Esegui()
{
throw new InvalidOperationException("Errore di elaborazione");
}
}
Alternative migliori nel codice moderno:
Task, che propaga eccezioni attraverso await;Il pattern producer-consumer separa chi produce dati da chi li consuma.
È fondamentale in:
Idea:
flowchart LR
P1["Producer 1"] --> Q["Queue condivisa"]
P2["Producer 2"] --> Q
Q --> C1["Consumer 1"]
Q --> C2["Consumer 2"]
Versione moderna e pulita con BlockingCollection<T>:
using System;
using System.Collections.Concurrent;
using System.Threading;
public class Program
{
public static void Main()
{
using BlockingCollection<string> jobs = new(boundedCapacity: 3);
Thread producer = new Thread(() =>
{
foreach (string job in new[] { "A", "B", "C", "D", "E" })
{
jobs.Add(job);
Console.WriteLine($"Accodato {job}");
}
jobs.CompleteAdding();
});
Thread consumer = new Thread(() =>
{
foreach (string job in jobs.GetConsumingEnumerable())
{
Console.WriteLine($"Elaboro {job}");
Thread.Sleep(500);
}
});
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
}
}
Perché è un pattern così importante:
Task, async/await e collezioni concorrenti per la maggior parte del codice applicativo;Thread solo quando ti serve davvero un thread dedicato;Interlocked per contatori e stati semplici;ThreadPool.SetMinThreads o SetMaxThreads;Task per propagarle.Thread.Sleep come meccanismo di sincronizzazione;lock(this) o lock("stringa");volatile pensando che renda atomico x++;Thread.Abort;List<T> o Dictionary<TKey, TValue> tra thread senza protezione;| Problema | Strumento consigliato |
|---|---|
| contatore condiviso | Interlocked |
| stato condiviso complesso | lock |
| molte letture, poche scritture | ReaderWriterLockSlim |
| limite massimo di accessi concorrenti | SemaphoreSlim |
| coordinare producer-consumer | BlockingCollection<T> |
| sincronizzare processi diversi | Mutex |
| thread dedicato a lunga vita | Thread |
| lavoro breve riusabile | ThreadPool / Task |
Studiare Thread in C# serve a capire tre livelli diversi:
Se memorizzi solo una regola, che sia questa:
Qual è la differenza fondamentale tra processi e thread?
Cosa fa Thread.Join()?
Perché Thread.Abort() è sconsigliato?
Quale affermazione descrive meglio il ThreadPool?
Una race condition richiede tipicamente che:
Quale insieme rappresenta le condizioni di Coffman per il deadlock?
Quando è più adatto Interlocked rispetto a lock?
Cosa garantisce volatile?
Quale collezione è pensata esplicitamente per producer-consumer con semantica di blocco?
Come vanno gestite correttamente le eccezioni nei thread manuali?
Scenario: Stai sviluppando il backend di un e-commerce. Gli ordini arrivano rapidamente dal sito web, ma la generazione delle etichette di spedizione richiede tempo. Vuoi evitare che il thread che riceve gli ordini resti bloccato.
Consegna:
OrderQueueService che usi BlockingCollection<int> come coda di ordini.Thread.Sleep(300).CompleteAdding().Interlocked.Obiettivo didattico: capire il pattern producer-consumer, l’uso di BlockingCollection<T> e la differenza tra produzione e consumo concorrente.
Scenario: Hai una cache di configurazioni applicative letta migliaia di volte al secondo da thread diversi, ma aggiornata solo occasionalmente da un processo di refresh.
Consegna:
ConfigurationCache basata su Dictionary<string, string>.ReaderWriterLockSlim.Get(string key)Set(string key, string value)RefreshDefaults()Obiettivo didattico: imparare quando ReaderWriterLockSlim è preferibile a un semplice lock in scenari read-heavy.
Scenario: Un’applicazione desktop deve importare centinaia di file CSV, ma il database di destinazione può gestire al massimo 3 scritture concorrenti senza degradare drasticamente.
Consegna:
ThreadPool con ThreadPool.QueueUserWorkItem.SemaphoreSlim(3) per limitare a 3 il numero massimo di import simultanei.Thread.Sleep.Interlocked.Increment.Obiettivo didattico: combinare ThreadPool, semafori e variabili atomiche per modellare un limite reale di concorrenza.
Scenario: Una banca digitale ha un bug intermittente: due operazioni concorrenti sullo stesso conto producono a volte un saldo finale errato.
Consegna:
BankAccount con campo _balance.Deposit(decimal amount) e Withdraw(decimal amount) senza sincronizzazione.lock privato.volatile non basta;Interlocked non è sufficiente se devi proteggere invarianti più complesse del semplice incremento.Obiettivo didattico: riconoscere una race condition reale e scegliere la primitiva di sincronizzazione corretta in base all’invariante da proteggere.
Prenota una lezione