1. Introduzione
Immagina che un thread sia un collaboratore instancabile a cui affidi del lavoro. Il collaboratore può dormire (non ha ancora iniziato), lavorare sodo (il tuo metodo è in esecuzione), aspettare che gli venga assegnato altro lavoro (idle), oppure finire il lavoro (terminare).
In C# (e in generale in .NET) il ciclo di vita di un thread consiste di diversi stati:
- Unstarted — il thread è stato creato, ma non è ancora avviato.
- Running — il thread è in esecuzione.
- WaitSleepJoin — il thread è temporaneamente inattivo (per esempio aspetta un segnale o "dorme").
- Stopped — il thread ha completato il suo compito ed è terminato.
Si può rappresentare visivamente questo ciclo con lo schema seguente:
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Running: Start()
Running --> WaitSleepJoin: Wait/Sleep/Join
WaitSleepJoin --> Running: Ricevuto segnale/Tempo scaduto
Running --> Stopped: Metodo terminato
WaitSleepJoin --> Stopped: Metodo terminato
Stopped --> [*]
Tutto inizia creando un oggetto Thread, ma finché non chiami Start(), il thread "dorme" nello stato Unstarted. Dopo Start() — si parte: il thread passa a Running. Se il thread chiama Thread.Sleep o sta aspettando qualcosa (per esempio Monitor.Wait), entra nello stato di attesa. Appena il metodo passato al thread termina, il thread muore, smette di esistere e non "risorgerà". Fine della storia.
2. Pratica: ciclo di vita di un thread semplice
Vediamo l'esempio classico:
using System;
using System.Threading;
class Program
{
static void Main()
{
// Creiamo il thread — per ora stiamo solo programmando il lavoro
Thread worker = new Thread(DoWork);
Console.WriteLine($"Stato del thread dopo la creazione: {worker.ThreadState}");
// Avviamo il thread
worker.Start();
Console.WriteLine($"Stato del thread dopo l'avvio: {worker.ThreadState}");
// Facciamo dormire il thread principale in modo che il worker abbia tempo per lavorare
Thread.Sleep(100);
Console.WriteLine($"Stato del thread (più tardi): {worker.ThreadState}");
// Aspettiamo che worker finisca (ci uniamo)
worker.Join();
Console.WriteLine($"Stato del thread dopo il completamento: {worker.ThreadState}");
Console.WriteLine("Thread principale terminato");
}
static void DoWork()
{
Console.WriteLine("Il thread di lavoro ha iniziato!");
Thread.Sleep(500);
Console.WriteLine("Il thread di lavoro ha terminato!");
}
}
Cosa stampa il programma?
- Dopo la creazione — lo stato sarà Unstarted.
- Dopo lo start — di solito subito Running (ma può essere anche Running | Background).
- Durante l'esecuzione — lo stato può essere Running, o WaitSleepJoin se il thread "dorme".
- Dopo la terminazione del metodo — lo stato diventa Stopped.
Questo codice è uno strumento utile per capire in quale stato può trovarsi il tuo thread. Puoi giocare con i ritardi e vedere come cambia lo stato.
3. Gestire il thread: metodi principali
Avvio: Start()
È ovvio, ma lo ripetiamo: crei un thread — avvialo con Start(). E puoi avviarlo solo una volta: tentare di chiamare Start() di nuovo lancerà un'eccezione ThreadStateException.
Thread t = new Thread(MyMethod);
t.Start(); // OK
t.Start(); // Errore!
Aspettare il completamento: Join()
A volte serve aspettare che un thread finisca prima di proseguire. Per questo c'è Join().
Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // Blocca il thread corrente fino al completamento di t
Se hai più thread, puoi chiamare Join() su ciascuno — il thread principale aspetterà finché tutti i "lavoratori" non finiscono.
Varianti: esiste l'overload Join(int millisecondsTimeout), che aspetta solo il tempo specificato e poi continua.
// Aspettiamo al massimo 2 secondi
if (t.Join(2000))
Console.WriteLine("Il thread è terminato in tempo");
else
Console.WriteLine("Abbiamo stancato di aspettare...");
Terminazione forzata: perché è una cattiva idea
Nelle versioni vecchie di .NET c'era il metodo Thread.Abort(), che permetteva di "uccidere" un thread al volo. Ora quasi non si usa più — è pericoloso e può lasciare l'app in uno stato incoerente. La filosofia .NET è: il thread dovrebbe terminare volontariamente. Non "ammazzi" il collaboratore — gli fai gentilmente capire che la giornata lavorativa è finita.
4. Come "fermare" correttamente un thread
Il modo più corretto e sicuro per fermare un thread è usare un flag di cancellazione o un indicatore di terminazione che il thread controlla periodicamente.
class Worker
{
private volatile bool shouldStop = false;
public void DoWork()
{
while (!shouldStop)
{
Console.WriteLine("Lavoro!");
Thread.Sleep(300);
}
Console.WriteLine("Il thread termina su richiesta.");
}
public void RequestStop()
{
shouldStop = true;
}
}
Esempio d'uso:
Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();
// Aspettiamo un po'
Thread.Sleep(1000);
// Chiediamo al thread di terminare
w.RequestStop();
t.Join(); // Aspettiamo la terminazione del thread
Punto importante: volatile
La parola chiave volatile dice al compilatore e alla CPU: "Non cache-are questo campo, prendi sempre il valore aggiornato!" Questo è importante perché il thread veda l'ultimo valore del flag di terminazione. Senza questo (o senza altri meccanismi di sincronizzazione) il thread potrebbe non notare mai le tue modifiche.
5. Transizione dei thread negli stati di attesa e sonno
A volte il thread non sta facendo lavoro — o dorme, o aspetta.
Sono: Thread.Sleep
Quando vuoi far riposare un thread o rallentare l'esecuzione (per esempio per non sovraccaricare la CPU), usa Thread.Sleep(milliseconds).
// Il thread dorme per 2 secondi
Thread.Sleep(2000);
Durante il sonno il thread non esegue lavoro.
Aspettare / Join
Quando il thread principale aspetta che un figlio termini (Join), il principale è "in pausa". Allo stesso modo, se un thread aspetta il rilascio di una risorsa (per esempio tramite monitor o altri primitive di sincronizzazione), passa in uno stato di attesa.
6. Gestire il background del thread
In .NET i thread sono di due tipi: foreground (primo piano) e background (secondo piano). La differenza è semplice:
- Se nel processo rimangono solo thread in background, il processo termina automaticamente.
- Il thread principale e tutti i thread in foreground devono terminare affinché il processo si chiuda.
Puoi indicare esplicitamente che un thread è background:
Thread t = new Thread(SomeMethod);
t.IsBackground = true; // Impostato come background
t.Start();
Esempio pratico — Demon vs Thread normale
Thread t = new Thread(() =>
{
while (true)
{
Console.WriteLine("Sono un fantasma (background), non mi puoi fermare!");
Thread.Sleep(500);
}
});
t.IsBackground = true; // Lo rendiamo background
t.Start();
Thread.Sleep(1200);
Console.WriteLine("Il thread principale termina");
// Dopo la fine di Main — il processo muore, e anche il nostro thread eterno scompare
Dopo che Main termina — il processo finisce; i thread in background vengono fermati automaticamente.
7. Sfumature utili
Cosa non fare con i thread
- Non puoi "riavviare" un thread. L'oggetto Thread vive una sola volta: quando il suo metodo è terminato — il thread è morto, e tentare di chiamare Start() di nuovo genera un errore.
- Non puoi forzare la terminazione di thread altrui con metodi come Thread.Abort() o Thread.Suspend() — sono obsoleti e pericolosi.
- Non ignorare la chiusura delle risorse al termine del thread. Se il thread lavora con file o risorse, assicurati di rilasciarle correttamente prima di terminare il thread.
Controllare lo stato e gestire il ciclo di vita
if (t.IsAlive)
{
Console.WriteLine("Il thread è ancora vivo");
}
else
{
Console.WriteLine("Il thread è terminato");
}
IsAlive è true finché il thread esegue il suo metodo; dopo la terminazione è false.
Ciclo di vita di un thread semplice in .NET
| Stato | Come entrarci | Cosa significa? | Come uscirne |
|---|---|---|---|
| Unstarted | |
Thread creato, non avviato | Chiamare Start() |
| Running | |
Il thread esegue lavoro | Terminare il metodo |
| WaitSleepJoin | Sleep(), Join(), attesa | Thread temporaneamente inattivo | L'attesa termina |
| Stopped | Il metodo del thread è terminato | Il thread è "morto" | Non esce — è la fine |
FAQ sulla gestione del ciclo di vita del thread
Domanda: Si può uccidere un thread su comando?
Risposta: No e non serve, i thread dovrebbero monitorare da soli la fine del lavoro. Usa flag di cancellazione.
Domanda: Si può riutilizzare un oggetto Thread?
Risposta: No. Crea un nuovo oggetto per un nuovo lavoro.
Domanda: Cosa succede se il thread principale termina e un thread figlio continua a lavorare?
Risposta: Se il thread figlio è in background (IsBackground == true), l'applicazione terminerà. Se non lo è — il processo rimarrà in vita finché tutti i thread non finiscono.
Domanda: Come pulire correttamente le risorse se il thread termina per cancellazione?
Risposta: Usa blocchi try...finally dentro il metodo del thread, così le risorse vengono rilasciate in ogni caso.
8. Errori tipici e come evitarli lavorando con i thread
Errore №1: riutilizzare lo stesso oggetto Thread.
Non puoi avviare lo stesso oggetto thread più di una volta. Dopo la terminazione non è possibile riavviarlo — questo genererà un'eccezione.
Errore №2: rilascio scorretto di risorse esterne nel thread.
Se il thread lavora con file, rete o altre risorse, assicurati di chiuderle e rilasciarle correttamente. È consigliato usare blocchi finally o costrutti using per evitare leak e blocchi.
Errore №3: creare un numero eccessivo di thread.
Troppi thread complicano il debugging e possono degradare le prestazioni. Un thread extra è tempo in più speso per trovare e risolvere bug inaspettati.
GO TO FULL VERSION