Programmazione asincrona con async e await in C#
Molte operazioni di un'applicazione comportano un'attesa: leggere un file, interrogare un database, chiamare un servizio web. Se il programma restasse bloccato durante ogni attesa, sprecherebbe risorse e, nelle interfacce utente, apparirebbe congelato. La programmazione asincrona risolve questo problema permettendo al codice di proseguire mentre un'operazione lenta si completa in background. C# rende tutto ciò sorprendentemente semplice con le parole chiave async e await, protagoniste di questo articolo.
Il problema delle operazioni bloccanti
Un'operazione sincrona blocca il thread che la esegue fino al suo completamento. Se quel thread è quello dell'interfaccia utente, l'applicazione smette di rispondere; se è un thread di un server, quel thread non può servire altre richieste. La programmazione asincrona libera il thread durante l'attesa, restituendogli il controllo affinché possa svolgere altro lavoro.
Il tipo Task
Al centro del modello asincrono di .NET c'è il tipo Task, che rappresenta un'operazione in corso che verrà completata in futuro. Un Task senza valore rappresenta un'operazione che non restituisce nulla, mentre Task<T> rappresenta un'operazione che, una volta completata, produrrà un valore di tipo T. Si può pensare a un task come a una promessa di un risultato futuro.
Le parole chiave async e await
Un metodo marcato con async può contenere l'operatore await, che sospende l'esecuzione del metodo finché il task atteso non si completa, senza però bloccare il thread. Quando il task termina, l'esecuzione riprende dal punto in cui era stata sospesa.
using System.Net.Http;
// Un metodo asincrono che restituisce una stringa in futuro
static async Task<string> DownloadContentAsync(string url)
{
using var client = new HttpClient();
// await sospende il metodo senza bloccare il thread
string content = await client.GetStringAsync(url);
return content;
}
Da notare la convenzione: i metodi asincroni terminano tipicamente con il suffisso Async, per segnalarne chiaramente la natura. Il tipo di ritorno è Task<string>, ma grazie ad await il chiamante lavora con il valore string come se fosse sincrono, mantenendo il codice lineare e leggibile.
Attendere e usare il risultato
Per consumare un metodo asincrono si usa a sua volta await, il che rende asincrono anche il metodo chiamante. Questa catena si propaga verso l'alto fino al punto di ingresso, che nelle applicazioni moderne può essere un Main asincrono.
static async Task Main()
{
Console.WriteLine("Avvio del download");
string content = await DownloadContentAsync("https://example.com");
Console.WriteLine($"Ricevuti {content.Length} caratteri");
}
Eseguire operazioni in parallelo
Uno dei maggiori vantaggi dell'asincronia è la possibilità di avviare più operazioni contemporaneamente e attenderne il completamento collettivo. Invece di attendere una alla volta in sequenza, si avviano tutti i task e li si attende insieme con Task.WhenAll, riducendo drasticamente il tempo totale.
static async Task DownloadAllAsync()
{
var urls = new[]
{
"https://example.com/a",
"https://example.com/b",
"https://example.com/c"
};
// Avvia tutti i download senza attenderli subito
IEnumerable<Task<string>> tasks = urls.Select(url => DownloadContentAsync(url));
// Attende il completamento di tutti in parallelo
string[] results = await Task.WhenAll(tasks);
Console.WriteLine($"Scaricate {results.Length} pagine");
}
Esiste anche Task.WhenAny, che si completa non appena il primo dei task forniti termina, utile ad esempio per interrogare più sorgenti e usare la risposta più rapida.
La gestione delle eccezioni nell'asincronia
Le eccezioni sollevate all'interno di un metodo asincrono si gestiscono con i normali blocchi try-catch attorno all'espressione await. Il modello asincrono si integra in modo naturale con la gestione degli errori vista in precedenza.
static async Task SafeDownloadAsync(string url)
{
try
{
string content = await DownloadContentAsync(url);
Console.WriteLine("Download riuscito");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Errore di rete: {ex.Message}");
}
}
Annullare un'operazione
Le operazioni asincrone lunghe dovrebbero poter essere annullate, ad esempio quando l'utente cambia idea o scade un tempo limite. Il modello standard usa un CancellationToken, che viene passato attraverso la catena di chiamate e verificato periodicamente per interrompere il lavoro in modo pulito.
static async Task ProcessAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
// Interrompe con un'eccezione se e stato richiesto l'annullamento
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100, cancellationToken); // simula lavoro
}
}
// Creazione di un token con annullamento dopo cinque secondi
using var source = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await ProcessAsync(source.Token);
Errori comuni da evitare
La programmazione asincrona ha alcune insidie che è bene conoscere fin dall'inizio:
- Evitare di bloccare un task chiamando proprietà come
.Resulto metodi come.Wait(): possono causare blocchi permanenti. Usare sempreawait. - Non dichiarare metodi asincroni con tipo di ritorno
void, tranne che per i gestori di eventi, perché le loro eccezioni non possono essere intercettate correttamente. - Propagare l'asincronia lungo tutta la catena anziché mescolare codice sincrono e asincrono in modo improprio.
Conclusione
Abbiamo introdotto la programmazione asincrona come soluzione al problema delle operazioni bloccanti, comprendendo il tipo Task come promessa di un risultato futuro e le parole chiave async e await che rendono il codice asincrono lineare e leggibile. Abbiamo visto come eseguire operazioni in parallelo con Task.WhenAll, gestire le eccezioni, annullare operazioni con un CancellationToken ed evitare gli errori più comuni. Nel prossimo articolo torneremo al sistema dei tipi esplorando enumerazioni, struct e tuple.