1. Introduzione
Se pensi al processo come a un supermercato, i thread (thread) sono le cassiere alle diverse casse che servono i clienti contemporaneamente. Tutte le cassiere lavorano nello stesso negozio, ma ognuna esegue il proprio compito in parallelo, quindi il lavoro procede più velocemente ed efficacemente. I thread all'interno di un processo permettono di eseguire più compiti nello stesso momento, condividendo risorse comuni e coordinando il lavoro dentro la stessa applicazione. Il vantaggio principale è che i thread possono davvero lavorare in parallelo, se la CPU supporta il multitasking.
Perché serve il multithreading nella pratica
Perché vorremmo lanciare più thread? Ecco un paio di situazioni reali:
- Stai scrivendo un'app con interfaccia grafica e non vuoi che si "blocchi" durante un'operazione lunga.
- Devi scaricare più file contemporaneamente.
- In un gioco i nemici devono “pensare” indipendentemente l'uno dall'altro.
È sorprendente, ma in molti programmi si trovano ancora blocchi dovuti a un uso inesperto dei thread. Oggi impariamo a evitare questi pasticci.
La classe Thread: la base del multithreading manuale
La classe Thread è un dinosauro nel panorama del multithreading in .NET. Nonostante siano arrivati strumenti più moderni (Task, async/await), lavorare con Thread ha ancora senso, specialmente se vuoi sentire la gestione dei thread "dall'inizio".
Schema per creare un thread
- Creare un oggetto della classe Thread, passando il metodo da eseguire nel thread.
- Avviare il thread con il metodo Start().
- (Opzionale) Controllare cosa succede — tutto potrebbe funzionare in parallelo!
2. Avviare un thread con Thread
Proviamo a sentire sulla pratica la magia del parallelismo. Aggiungiamo alla nostra app un piccolo classe che conta fino a un certo numero e stampa il progresso. Impareremo ad avviare questo lavoro in un thread separato.
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("Il thread principale è partito!");
// Creiamo l'oggetto thread indicando il metodo da eseguire
Thread workerThread = new Thread(CountToTen);
// Avviamo il nuovo thread
workerThread.Start();
// Anche il thread principale fa qualcosa: stampa punti...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // Ritardo per chiarezza
}
Console.WriteLine("\nIl thread principale è terminato!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[Thread] Contiamo: {i}");
Thread.Sleep(400);
}
Console.WriteLine("[Thread] Fatto!");
}
}
Cosa succede?
Vedrai nella console che i punti e "Contiamo: X" appaiono mescolati. Questo è il primo segno del multithreading! Il thread principale stampa i suoi punti e il nuovo thread conta fino a 10. Non si disturbano a vicenda, come due musicisti in una band: uno suona la batteria, l'altro il pianoforte. Ognuno suona la sua parte e insieme viene fuori musica.
3. Come passare dati al thread?
A volte il thread deve sapere non solo cosa fare, ma anche su cosa lavorare. Se il metodo del thread prende parametri, come li passiamo?
Opzione 1: usare una lambda (metodo anonimo)
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[Thread] {i} / {n}");
Thread.Sleep(300);
}
}
Qui avvolgiamo la chiamata al metodo desiderato in una lambda per passare i parametri. Questa è una pratica molto comune, perché Thread si aspetta un metodo senza parametri (ThreadStart).
Opzione 2: usare ParameterizedThreadStart
Si può usare il delegato speciale ParameterizedThreadStart, che accetta un parametro di tipo object.
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[Thread] {i} / {max}");
Thread.Sleep(200);
}
}
Sì, il tipo del parametro è object, quindi serve il cast. Non è il massimo, ma funziona! Tuttavia il C# moderno preferisce la variante con lambda.
4. Gestire la vita del thread
Vediamo quali strumenti utili fornisce la classe Thread.
| Proprietà / Metodo | Scopo |
|---|---|
|
Avvia il thread (il metodo specificato alla creazione) |
|
Attende la terminazione del thread (blocca il thread chiamante fino al termine dell'altro thread) |
|
Mostra se il thread è in esecuzione ora (true/false) |
|
Permette di dare un nome al thread (utile per il debugging) |
|
Ottieni l'oggetto che rappresenta il thread corrente |
|
Ferma il thread corrente per ms millisecondi |
Esempio: aspettiamo la terminazione di un thread
Talvolta è necessario che il thread principale aspetti la fine di un thread secondario.
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[Secondo thread] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[Thread principale] Aspettiamo la fine del secondo thread...");
t.Join(); // Il thread principale aspetta qui
Console.WriteLine("[Thread principale] Il secondo thread è terminato!");
Senza Join() il programma potrebbe finire anche se il thread è ancora in esecuzione. Con Join() il thread principale aspetterà pazientemente che tutto sia concluso.
5. Sfumature utili
Denominare i thread: per non confondersi
Per il debugging puoi dare un nome ai thread:
Thread t = new Thread(() =>
{
Console.WriteLine($"Questo viene eseguito nel thread: {Thread.CurrentThread.Name}");
});
t.Name = "Thread-Contatore";
t.Start();
Questo aiuta quando i thread sono molti e ognuno fa la sua cosa.
Limitazioni e futuro reale
È importante dire subito: nelle applicazioni moderne il controllo manuale dei thread tramite Thread è raro. Nella pratica si usano strumenti più potenti e intelligenti (Task, async/await), che vedremo più avanti. Ma capire le basi del funzionamento dei thread è importante per:
- Capire il "dietro le quinte" di C# e .NET.
- Colloqui tecnici (a volte ti chiedono la differenza tra Thread e Task).
- Diagnosticare e risolvere problemi complessi in grandi applicazioni ereditate.
Schema riassuntivo: ciclo di vita di un thread
stateDiagram-v2
[*] --> New: Thread creato
New --> Running: Start()
Running --> Stopped: Metodo terminato
Stopped --> [*]
Ora sapete creare e avviare thread in C#. Non siete più solo passeggeri sul treno, siete il macchinista che controlla più carrozze! Davanti a voi: ciclo di vita del thread, la sua gestione, sincronizzazione e nuovi orizzonti del parallelismo.
6. Errori tipici e trucchi quando si lavora con Thread
Errore n.1: non avviare il thread.
Spesso si crea l'oggetto Thread ma si dimentica di chiamare Start(). Di conseguenza il thread non parte, e capire il motivo non è immediato.
Errore n.2: modificare dati condivisi senza sincronizzazione.
Se più thread lavorano sulla stessa variabile senza protezione, aspettati problemi! È come se due cassieri dessero il resto dalla stessa scatola — presto arriva il caos e gli errori.
Errore n.3: usare metodi deprecati e pericolosi.
Non usare Thread.Suspend(), Thread.Resume() e simili — sono considerati pericolosi e deprecati. Gestisci il ciclo di vita dei thread in altri modi.
Errore n.4: eccezioni non gestite dentro i thread.
Se un'eccezione avviene in un thread e non viene catturata, il thread terminerà e il thread principale potrebbe non accorgersene! Avvolgi il codice del thread in un blocco try-catch per intercettare e loggare gli errori.
Thread t = new Thread(() =>
{
try
{
// ... il vostro codice
}
catch (Exception ex)
{
// Logghiamo l'errore
Console.WriteLine($"[Thread] Errore: {ex.Message}");
}
});
t.Start();
GO TO FULL VERSION