1. Introduzione
L'asincronia in C# è uno strumento potente. Ma a volte capita la situazione in cui il codice asincrono deve essere chiamato da codice sincrono (o viceversa). Sembra che tutto debba "funzionare magicamente", ma nella pratica possono verificarsi strani blocchi (deadlock), perdita di prestazioni e persino, inaspettatamente… interfacce utente rovinate. Spesso il bug si manifesta solo con dati reali: in produzione o dall'utente. La causa spesso sta in un'interazione sbagliata tra codice sincrono e asincrono e in dettagli non ovvi del funzionamento dello scheduler dei task di .NET.
Inoltre le librerie e i framework moderni fanno ampio uso di metodi asincroni, e dovete capire come "incollare" correttamente il codice asincrono nelle catene sincrone esistenti o, al contrario, come chiamare correttamente codice sincrono da un metodo asincrono.
Breve ripasso: cosa succede con await
Quando scrivi:
await SomeAsyncMethod();
Il codice viene "spezzato" in due parti: prima di await e dopo. La prima parte viene eseguita fino alla prima operazione asincrona che attende (per esempio una richiesta di rete), il seguito viene eseguito dopo il suo completamento. Domanda: dove verrà eseguito il "continuation"? Sullo stesso thread? Su un altro? Se stiamo scrivendo un'app desktop (per esempio WPF o WinForms), o una console? La risposta — dipende. Qui entra in gioco, ad esempio, ConfigureAwait.
Cos'è SynchronizationContext?
SynchronizationContext è un meccanismo speciale di .NET che permette al codice di "ricordare" dove e come deve essere invocato il proseguimento di un'operazione asincrona.
- Nelle applicazioni classiche WinForms/WPF SynchronizationContext garantisce che dopo await la parte rimanente del metodo continui a eseguirsi sullo stesso thread UI, evitando errori di "accesso a un controllo da un thread diverso".
- In ASP.NET (vecchio, non Core) SynchronizationContext permette di ripristinare HttpContext e continuare a lavorare con la richiesta HTTP.
- Nelle applicazioni Console e in ASP.NET Core il SynchronizationContext di solito non esiste (è null), e i continuation vengono eseguiti sul thread pool.
Cos'è TaskScheduler?
TaskScheduler è un meccanismo di livello più basso. Nella maggior parte dei casi lavorate con TaskScheduler.Default, che usa il thread pool di .NET. È responsabile di quali task vengono eseguiti, quando e dove.
2. Deadlock quando si mescolano await e Result/Wait()
Una delle trappole più famose in C#:
// Da qualche parte nel codice UI
var result = SomeAsyncMethod().Result;
o
SomeAsyncMethod().Wait();
Tutto: l'applicazione "si è bloccata". Perché?
Come succede?
- Chiami un metodo asincrono e subito chiedi .Result o .Wait() — cioè "blocchi" il thread corrente e aspetti la fine del task asincrono.
- SomeAsyncMethod internamente fa await, e pianifica il continuation sullo stesso thread tramite SynchronizationContext, ma quel thread è già bloccato in attesa di Result/.Wait().
- Poiché il thread sta aspettando il completamento di Result/.Wait(), non può eseguire il continuation.
- Tutto bloccato: deadlock — il thread aspetta sé stesso.
Questo è particolarmente facile da riprodurre nelle applicazioni UI, dove tutto gira sul thread dell'interfaccia e tutti i continuation aspettano proprio quel thread. Nelle console e in ASP.NET Core (senza context) questi deadlock si vedono raramente.
Battuta dalla vita reale: se sei riuscito a ottenere un deadlock con .Result — congratulazioni, sei a un paio di passi dal titolo di Senior :D
3. Come integrare correttamente codice asincrono in codice sincrono?
Raccomandazione numero uno: asincronia dall'alto in basso
Se hai un metodo asincrono, riporta async/await su nello stack di chiamate fino alla UI o al punto di ingresso. Non tenere "l'asincronia" a metà strada.
Male (blocca il thread):
// Metodo sincrono chiama async via .Result
public void DoStuff()
{
var data = GetDataAsync().Result;
// ...
}
Bene (asincronia end-to-end):
public async Task DoStuffAsync()
{
var data = await GetDataAsync();
// ...
}
Se possibile — usa sempre await, non .Result / .Wait().
4. Ma ci sono situazioni: bisogna chiamare async da sync
Riscrivere tutto il ramo superiore in async (la soluzione migliore, se hai scelta).
Usare pattern speciali: per esempio eseguire il task su un thread separato con Task.Run, e dentro chiamare il metodo async.
public void DoStuff()
{
var result = Task.Run(() => SomeAsyncMethod()).Result;
}
Però: anche qui ci sono sfumature con la sincronizzazione nelle app UI, quindi è meglio non farlo senza una buona ragione.
5. Cosa fa ConfigureAwait(false)?
A volte non ti interessa che dopo await il codice continui sullo stesso thread (per esempio in applicazioni server o in una libreria dove il SynchronizationContext non è rilevante). Anzi, è preferibile che .NET non si leghi a un thread specifico — migliora le prestazioni!
Sintassi e principio
await SomeAsyncMethod().ConfigureAwait(false);
- ConfigureAwait(false) dice: "non ho bisogno del SynchronizationContext nativo, continua l'esecuzione ovunque, anche su un altro thread".
- ConfigureAwait(true) (di default) — "continua l'esecuzione dove è stato chiamato l'await, preferibilmente nello stesso SynchronizationContext".
Schema visivo
┌─────────────────────────────────────────────────────┐
│ SynchronizationContext │
└─────────────────────────────────────────────────────┘
↑ ↑
(UI-thread) await SomeAsyncMethod() Continuation (dopo await)
──────────────────────────────> (stesso thread — se ConfigureAwait(true))
↓
(qualsiasi thread — se ConfigureAwait(false))
Esempio di uso di ConfigureAwait(false) — codice "libreria"
Immagina di scrivere una libreria che può essere usata da chiunque: WinForms, WPF, ASP.NET, console…
Non devi dipendere dal loro modello di threading. Quindi usa sempre ConfigureAwait(false) nei metodi asincroni della tua libreria:
public async Task<string> LoadDataFromUrlAsync(string url)
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url).ConfigureAwait(false);
return content;
}
Ora il tuo metodo non "richiederà" di essere eseguito su uno specifico SynchronizationContext. È più sicuro e più performante (meno switch di thread).
Esempio: cosa succede con await senza ConfigureAwait
Consideriamo un'app WPF:
private async void Button_Click(object sender, RoutedEventArgs e)
{
Button1.Content = "Caricamento...";
await Task.Delay(2000); // Simulazione operazione lunga
Button1.Content = "Fatto!";
}
Task.Delay al suo interno fa "await". Di default dopo il await il controllo ritorna sul thread UI, così puoi aggiornare i controlli.
Se dentro la tua lunga operazione usi ConfigureAwait(false):
await Task.Delay(2000).ConfigureAwait(false);
Button1.Content = "Fatto!"; // Errore!
Si genererà un'eccezione: InvalidOperationException: "The calling thread cannot access this object because a different thread owns it."
Perché ora il "continuation" viene eseguito su un thread diverso e non si può accedere alla UI.
Conclusione: usa ConfigureAwait(false) solo dove non servono accessi alla UI/contesto.
6. Sfumature utili
Dove e quando usare ConfigureAwait
| Scenario | Devo usare .ConfigureAwait(false)? | Perché? |
|---|---|---|
| Codice di libreria | Sì | Viene chiamato in qualunque contesto, la UI non è necessaria |
| Dentro codice ASP.NET Core | Sì (il context quasi non c'è) | Aumenta le prestazioni |
| In WinForms/WPF, quando si accede alla UI | No | Devi riportare il controllo sul thread UI |
| Metodi sincroni | Non ha senso | Non c'è SynchronizationContext |
| In applicazioni Console | Puoi, ma non si vede effetto | Non c'è context |
Come sapere se c'è SynchronizationContext?
Puoi controllarlo direttamente dal codice:
Console.WriteLine(SynchronizationContext.Current == null
? "Nessun contesto"
: "Contesto esiste");
- Nelle applicazioni Console e in ASP.NET Core sarà "Nessun contesto".
- In WinForms/WPF — "Contesto esiste".
Visualizzazione del passaggio tra thread
sequenceDiagram
participant MainThread as Thread principale (UI/Console)
participant ThreadPool as Thread dal pool
MainThread->>SomeAsyncMethod: Chiamata metodo
SomeAsyncMethod->>MainThread: await senza ConfigureAwait
Note right of MainThread: Dopo await torniamo sullo stesso thread
SomeAsyncMethod->>ThreadPool: await con ConfigureAwait(false)
Note right of ThreadPool: Dopo await si può lavorare su qualsiasi thread
Regole rapide d'uso
- Non usare .Result e .Wait() nel codice UI con metodi asincroni.
- Nel codice di libreria usa rigorosamente ConfigureAwait(false) per tutti gli await.
- Nelle app UI usa ConfigureAwait(false) solo dentro metodi che non accedono ai controlli dell'interfaccia.
- Meglio non mescolare codice sincrono e asincrono senza una necessità.
- Se devi chiamare async da sync — pensa due volte: non puoi trasformare tutta la catena in async?
7. Errori tipici dei principianti con l'asincronia
Errore №1: uso diffuso di .Result o .Wait().
Molto spesso gli sviluppatori, specialmente dopo aver letto qualche articolo sull'asincronia, iniziano ad aggiungere .Result o .Wait() ovunque per "sincronizzare" le chiamate ai metodi asincroni. A prima vista sembra comodo, ma in pratica è la strada garantita verso il deadlock nelle app UI. È particolarmente pericoloso se dentro i metodi asincroni non si usa ConfigureAwait(false) — il thread UI si blocca e non può eseguire il continuation del task.
Errore №2: uso scorretto o eccessivo di ConfigureAwait(false).
Alcuni principianti applicano ConfigureAwait(false) ovunque, anche nel codice che manipola l'interfaccia. Nelle app UI questo porta a errori di accesso ai controlli (InvalidOperationException), perché il continuation verrà eseguito su un thread diverso dal thread UI.
Errore №3: dimenticare che ConfigureAwait funziona solo con oggetti awaitable.
Molti pensano che si possa usare ConfigureAwait(false) su qualsiasi metodo, ma in realtà funziona solo con oggetti che ritornano Task, Task<T>, ValueTask e altri tipi awaitable. Per metodi sincroni ConfigureAwait non ha effetto e l'attesa di tali metodi non diventa asincrona.
Errore №4: mescolare sincrono e asincrono senza bisogno.
Tentare di chiamare un metodo asincrono da codice sincrono senza una strategia ponderata (per esempio tramite .Result, .Wait() o Task.Run) spesso porta a bug sottili, perdita di performance e deadlock difficili da diagnosticare. Anche se il metodo sembra breve e sicuro, tali chiamate nella catena possono compromettere l'applicazione.
Errore №5: sottovalutare l'impatto del SynchronizationContext.
I principianti spesso dimenticano che il continuation dopo await può essere eseguito sullo stesso thread (UI) se non si usa ConfigureAwait(false). Questo porta a comportamenti imprevedibili, specialmente quando si combinano codice UI e codice di libreria, dove attente e continuation si intrecciano.
GO TO FULL VERSION