1. Introduzione
Chiamata bloccante — è qualsiasi chiamata che “blocca” l'esecuzione del thread finché non completa qualche operazione. Di solito è:
- Lettura o scrittura di dati (per esempio, su disco).
- Richieste lunghe al database o alla rete.
- Chiamate a librerie esterne che funzionano “lentamente”.
In quei momenti il thread sta semplicemente fermo, si gira le dita e aspetta: “E allora, quando arriva la risposta?”
Esempio pratico
// La tua logica principale
Console.WriteLine("Aspetto la risposta dal server...");
string response = CallServer(); // qui tutto si è bloccato!
Console.WriteLine("Il server ha risposto: " + response);
Finché il server non risponde, il tuo thread è "bloccato" — e non può fare altro!
Come si manifesta nelle applicazioni
Nei diversi tipi di applicazioni questo si vede in modo differente. In una console sembra che l'app sia semplicemente “ferma” — il cursore smette di lampeggiare e non c'è reattività. In un'interfaccia grafica, per esempio WinForms o WPF, la finestra improvvisamente smette di rispondere, appare il noto “not responding” e l'utente è pronto a maledire il tuo software e il tuo nome. Nei server la situazione è ancora peggiore: un thread bloccato significa un utente in meno servito.
2. Chiamate bloccanti in C# nella pratica
Mettiamoci le mani addosso usando un progetto di esempio. Supponiamo di avere un'app della sfera magica. Potrebbe esserci un frammento come questo:
Console.WriteLine("Inserisci la tua domanda per la sfera magica:");
string question = Console.ReadLine(); // chiamata bloccante: aspettiamo l'input!
Console.WriteLine("Sto pensando alla risposta...");
Thread.Sleep(5000); // Emulazione di lavoro lungo
Console.WriteLine("Risposta: prova di nuovo domani!");
- Console.ReadLine() — aspetta l'input dell'utente per tutto il tempo necessario (bloccando il thread).
- Thread.Sleep(5000) — forza una pausa, congela il thread.
Domanda: Che male c'è se lavoriamo solo con un'app console?
Risposta: In una console non è così critico — l'utente si aspetta di aspettare. Ma se l'utente non è uno solo? Se è un server che riceve subito 1000 richieste? O un'app GUI dove l'utente si aspetta reattività, non le tue riflessioni filosofiche?
Esempio: Blocco della UI
// In WinForms handler del bottone:
private void button1_Click(object sender, EventArgs e)
{
label1.Text = "Caricamento...";
DoHeavyWork(); // operazione pesante (per esempio, query al DB)
label1.Text = "Fatto!";
}
Problema: Mentre DoHeavyWork gira, la finestra non aggiorna l'interfaccia, non risponde a tastiera e mouse, non si ridisegna. Se l'utente prova a chiudere la finestra — Windows mostrerà "non risponde" e suggerirà di terminare l'app.
3. Il grande problema del mondo multithread
Problemi delle chiamate bloccanti
- Spreco di risorse. Ogni thread (Thread) in .NET è un oggetto piuttosto “pesante”. Il SO gli assegna memoria per lo stack (di solito 1 MB), handle, sincronizzazione ecc. Se un thread “sta fermo” (aspetta file o rete), sta comunque consumando risorse (“non facendo nulla, mangia memoria”).
- Numero limitato di thread. Nelle app server di solito c'è un pool di thread. Se tutti i thread sono bloccati — le nuove richieste non vengono servite. Per esempio in ASP.NET: se tutti i thread aspettano il DB, il sito “si blocca” per i nuovi utenti.
- Cattiva reattività dell'interfaccia. Nelle app GUI la finestra smette di rispondere anche al “chiudi” quando il UI-thread è bloccato.
- Calo di performance. Più thread sono bloccati, più context switch, più carico sul SO, più lento diventa tutto il sistema.
Fonti tipiche di chiamate bloccanti
Vediamo quali chiamate possono bloccare un thread “così, dal nulla”:
- Operazioni di rete: chiamate API, download di file, update.
- Disco/file system: lettura o scrittura di file grandi.
- Database: query lente a SQL Server o anche DB locali.
- Input utente: una lunga ReadLine — è una cosa piccola, ma blocca comunque.
- Thread.Sleep, Task.Delay: congelano il thread.
Esempio (scaricare dati da un sito)
using System.Net.Http;
HttpClient client = new HttpClient();
string result = client.GetStringAsync("https://google.com").Result; // chiamata sincrona bloccante!
Console.WriteLine(result);
Cosa succede qui? Il metodo .Result blocca il thread finché non arriva la risposta!
4. Come capire se la tua chiamata blocca il thread?
Semplice: se il metodo non restituisce il controllo prima di aver finito il suo lavoro (attesa, input, rete, disco) — è una chiamata bloccante.
- I metodi con Thread.Sleep, Task.Wait, .Result, .Wait() — bloccano.
- I metodi che fanno lettura/scrittura senza asincronia — idem.
- Una finestra “appesa” o un server che non risponde — segnale tipico.
Elemento visivo: Diagramma a blocchi di una chiamata bloccante
+-------------------+
| Avvio del metodo |
+-------------------+
|
v
+-------------------+
| Chiamata a metodo |
| bloccante (es.: |
| lettura file) |
+-------------------+
|
v
+-------------------+
| Aspettiamo il |
| completamento |
| dell'operazione |
+-------------------+
|
v
+-------------------+
| Ritorno dal metodo|
| e continuiamo |
+-------------------+
5. Perché le chiamate bloccanti sono dannose nei server
La maggior parte delle applicazioni moderne è di rete o server. Anche se scrivi un gioco, spesso c'è il caricamento dal server. Se fai un sito web, ci sarà sicuramente rete e disco.
Immagina che il tuo server gestisca 1000 richieste al secondo. Ogni richiesta deve parlare col DB. Scrivi:
// Handler web
string data = db.ReadDataSync(); // Blocco sincrono!
return new Response(data);
Mentre la query al DB è in corso, il thread sta fermo e aspetta. Una richiesta — ok. Ma quando si accumulano, i thread si riempiono. Le nuove richieste non riescono ad entrare e restano in coda. Il server smette di essere una macchina e diventa una lunga fila dove tutti stanno e sbadigliano aspettando che qualcosa si sblocchi.
Illustrazione: "Elaborazione sincrona delle richieste"
Richiesta 1: occupa thread -> aspetta DB -> rilascia
Richiesta 2: occupa thread -> aspetta DB -> rilascia
...
Richiesta 100: nessun thread libero, aspetta in coda...
6. Asincronia — la cura per le chiamate bloccanti
Per combattere il blocco si usano le chiamate asincrone. In C# le parole magiche sono: async e await.
Queste permettono di:
- Non bloccare il thread (soprattutto UI o thread server preziosi).
- Non occupare risorse inutilmente.
- Migliorare la reattività della UI.
- Non “tenere” un thread occupato mentre c'è un'operazione lenta in corso.
Nota: non cominciamo subito a usare l'asincronia senza capire, perché richiede comprensione di thread e attese. Ora che hai capito le pene delle chiamate bloccanti, l'approccio asincrono ti sembrerà una vera soddisfazione per te e per l'utente.
Tabella: Cosa blocca e cosa no?
| Metodo/Chiamata | Blocca il thread | Adatto per UI/Server |
|---|---|---|
|
Sì | No |
|
Sì | No |
|
Sì | No |
|
Sì | No |
|
Sì | No |
|
No | Sì |
|
No | Sì |
|
No | Sì |
NB: await funziona solo dentro una funzione asincrona (async Task ...), che analizzeremo meglio nelle prossime lezioni.
7. Errori leggendari quando si lavora con chiamate bloccanti
“Blocchi la UI — l'utente ti maledice”
private void btnLoad_Click(object sender, EventArgs e)
{
var data = BigFileReader.Read(@"C:\huge.dat"); // chiamata bloccante!
textBox1.Text = data;
}
Finché non viene letto il file enorme — la finestra resta “appesa”.
“Server bloccato — i clienti scappano”
public IActionResult Download()
{
var content = File.ReadAllBytes("bigfile.zip"); // sincrono, lungo, blocca il thread di ASP.NET!
return File(content, "application/zip");
}
I server piccoli (per esempio su hosting gratuito) possono semplicemente “crollare” con questo approccio.
“Metodo asincrono + .Wait()/.Result = morte sincrona”
public void LoadData()
{
var result = DoAsyncWork().Result; // Blocca il thread!
}
.Result e .Wait() trasformano anche il codice asincrono più elegante in una banale chiamata bloccante.
GO TO FULL VERSION