1. Introduzione
Il multithreading è come il lavoro parallelo di più colleghi in ufficio: uno stampa documenti, un altro chiama il cliente, il terzo prepara il caffè (e, ovviamente, sono tutti programmatori). Se avessimo un solo lavoratore, farebbe tutto in sequenza e l'ufficio si soffocherebbe dalla noia e dalla fila per il caffè. In programmazione la situazione è analoga: un programma single-thread può eseguire solo un'attività alla volta.
Immagina che la nostra applicazione stia eseguendo un'operazione lunga, per esempio scaricare un file da internet o calcolare una tabella enorme. Tutto il resto in quel momento si "congela" — i pulsanti non rispondono, le animazioni non si muovono, compaiono finestre con scritto "non risponde".
Il multithreading permette all'applicazione di fare più cose contemporaneamente: l'interfaccia resta reattiva, le operazioni vengono eseguite in parallelo e non ci ritroviamo a urlare "Computer, di nuovo sei bloccato?!".
Concetti base e terminologia
Prima di tuffarci a capo fitto, capiamo cos'è un thread e in cosa differisce da un processo.
- Processo (Process): Un programma indipendente con il proprio spazio di indirizzi, variabili e risorse. Per esempio, ogni applicazione lanciata su Windows è un processo separato.
- Thread (Thread): Un'unità di esecuzione all'interno di un processo. Un processo può contenere uno o più thread che condividono le stesse risorse (memoria, variabili).
Perché il multithreading crea tanti dubbi?
Perché i thread sono tipi imprevedibili: possono partire in qualsiasi momento, mischiare i dati, interrompersi a vicenda e creare scompiglio nella memoria se non si controlla l'ordine. Se ti sembra simile a un asilo senza educatore — esatto! Disciplina e cura nella gestione dei thread sono la base per scrivere programmi multithread sicuri.
2. Storia e ruolo del multithreading in C# e .NET
Ai tempi lontani C# era single-threaded e i programmi erano semplici. Con l'aumento delle esigenze di performance, l'avvento di processori multicore e la necessità di creare applicazioni reattive senza bloccare l'interfaccia, in .NET sono arrivate le API per il multithreading. All'inizio c'era il classico System.Threading.Thread, poi sono arrivati i Task (Task), i metodi asincroni (async/await), l'elaborazione parallela dei dati (PLINQ) e primitive di sincronizzazione di alto livello.
C# è cresciuto in una piattaforma potente dove il multithreading non è un'eccezione, ma ordinaria amministrazione.
Visivamente: processo e thread
Ecco uno schema semplice:
+--------------------------------------------------+
| Processo (il tuo programma) |
| +-------------+ +-------------+ |
| | Thread 1 | | Thread 2 | |
| +-------------+ +-------------+ |
| ... |
| +-------------+ |
| | Thread N | |
| +-------------+ |
+--------------------------------------------------+
Tutti i thread all'interno del processo vedono variabili e risorse condivise.
3. Come creare un thread in C#?
Cominciamo dal più semplice: la classe Thread nello namespace System.Threading.
Esempio: Avviamo un secondo thread
Supponiamo di avere un'attività lunga — per esempio calcolare la somma dei numeri da 1 a 10_000_000. Mentre il calcolo è in corso, il thread principale scrive un saluto all'utente.
using System;
using System.Threading;
class Program
{
// Metodo per il secondo task
static void CalculateSum()
{
long sum = 0;
for (int i = 1; i <= 10_000_000; i++)
sum += i;
Console.WriteLine($"[Thread 2] Somma: {sum}");
}
static void Main()
{
// Creiamo il thread, passiamo il delegate al metodo
Thread thread = new Thread(CalculateSum);
thread.Start(); // Avviamo il secondo thread
// Il thread principale continua a lavorare
Console.WriteLine("[Thread 1] Ciao! Lavoriamo in parallelo...");
// Aspettiamo il termine del secondo thread prima di uscire
thread.Join();
Console.WriteLine("[Thread 1] Tutto completato!");
}
}
Cosa succederà?
Sullo schermo apparirà la riga "[Thread 1] Ciao! Lavoriamo in parallelo...", e poi, quando il thread che esegue il calcolo termina, stamperà la somma.
Problema tipico: chi arriva prima stampa
Prova a eseguire questo codice più volte — l'ordine delle righe può cambiare! A volte la somma appare prima, altre volte il saluto. Questa è la vera natura del multithreading — i programmi diventano meno predicibili, come l'umore di un gatto il lunedì.
4. Area di memoria: cosa vedono i thread?
Tutti i thread all'interno di un processo hanno accesso alle stesse variabili (se non sono locali al metodo). Se una variabile viene modificata in un thread, gli altri la vedranno!
Esempio: Variabile condivisa
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Increment()
{
for (int i = 0; i < 1000; i++)
counter++;
}
static void Main()
{
Thread t1 = new Thread(Increment);
Thread t2 = new Thread(Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"counter = {counter}");
}
}
Cosa ci aspettiamo di vedere in counter? La logica suggerisce 2000, dato che ogni thread incrementa 1000 volte.
Invece no! Esegui più volte — vedrai valori diversi: 1782, 1935, 1999…
Perché? È il classico problema della Race Condition — i thread "si rubano" il controllo tra lettura -> incremento -> scrittura, e alcuni incrementi vanno persi.
5. Sfumature utili
Come interagiscono i thread con l'interfaccia?
Nelle applicazioni desktop moderne (WinForms/WPF/MAUI) il thread principale gestisce l'interfaccia grafica. Tutte le azioni dell'utente (click, input) avvengono in questo thread. I task in background dovrebbero essere eseguiti in altri thread, ma secondo le regole non si può "toccare" direttamente l'UI da thread diversi. Questo evita il caos.
In una console non c'è questa restrizione, si può chiamare Console.WriteLine da qualsiasi thread. Tuttavia, nelle applicazioni reali senza la giusta sincronizzazione l'interfaccia può "impazzire".
Sequenzialità e parallelismo
Tabella — per fissare le differenze.
| Codice single-thread | Codice multithread |
|---|---|
| Esegue i task in sequenza | I task possono essere eseguiti contemporaneamente |
| UI "si blocca" durante operazioni lunghe | UI resta reattiva |
| Leggere e scrivere variabili è semplice | Serve controllare l'accesso ai dati |
| Si fa debugging facilmente | Può essere difficile fare debugging |
Punti importanti quando si lavora con i thread
- Variabili condivise — rischio condiviso. Come abbiamo visto, se più thread usano una variabile condivisa — senza sincronizzazione si rischiano errori! (Approfondiremo nelle lezioni successive.)
- Si può "aspettare" un thread con Join(). Il metodo Join() permette di sospendere il thread principale fino al termine di quello di background. Usalo se devi aspettare un risultato.
- Un thread non può essere avviato due volte. Dopo l'esecuzione non puoi riavviare lo stesso thread — dovrai creare un nuovo oggetto Thread.
- Terminazione del thread. Un thread termina quando finisce l'esecuzione del suo metodo. Uccidere forzatamente i thread è una cattiva idea (il metodo Abort() è considerato pericoloso e obsoleto).
A cosa serve il multithreading nella pratica?
- Applicazioni UI: non bloccare l'interfaccia quando si fanno caricamenti o calcoli in background.
- Server e servizi: gestire contemporaneamente le richieste di molti client.
- Calcoli ad alte prestazioni: dividere un grande compito (es. elaborazione di milioni di record) in parti ed eseguirle in parallelo.
- Giochi, simulazioni, data processing: modellare sistemi complessi senza perdere performance.
Problemi del multithreading
Il multithreading dà potenza, ma aggiunge complessità:
- Race Condition (stato di gara): quando più thread modificano gli stessi dati contemporaneamente, il risultato dipende dall'ordine delle istruzioni, che è imprevedibile.
- Deadlock (blocco reciproco): i thread si aspettano a vicenda e nessuno può proseguire.
- Starvation (fame): uno dei thread viene sistematicamente privato dell'accesso a una risorsa.
In questa lezione abbiamo solo menzionato i problemi principali del multithreading; nelle lezioni successive impareremo a riconoscerli ed evitarli. Per ora — ricorda che se tutto "si blocca", la colpa potrebbe non essere solo un bug normale, ma thread che hanno "fatto festa".
GO TO FULL VERSION