1. Task.WhenAll: aspettare tutte insieme
Nella vita reale è raro fare solamente una cosa alla volta. Ti alzi dal letto e guardi l'orologio, fai la doccia e canti, ti vesti e ascolti musica mentre prepari il caffè. O sul computer scarichi più file contemporaneamente, invii una serie di richieste di rete, esegui calcoli diversi. Tutte queste azioni possono (e dovrebbero!) essere eseguite in parallelo per non far perdere tempo all'utente. Non vogliamo aspettare che ogni task finisca una dopo l'altra! Come organizzare questa "orchestra"? Come capire quando TUTTO è completato? O, al contrario, sapere chi ha finito per primo?
Task.WhenAll e Task.WhenAny sono gli strumenti giusti per questi scenari.
Metodo WhenAll
Task.WhenAll è un metodo statico della classe Task che prende una collezione di task e restituisce una nuova task che si completa solo quando tutte le task passate sono terminate. È come se all'esame aspettassi che tutti i compagni restituissero i biglietti prima di uscire dall'aula.
Firma del metodo
Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)
Ci sono versioni per le task che ritornano un risultato (Task<TResult>) e per le "vuote" (Task).
Esempio più semplice: aspettare che tutti i file siano scaricati
Proviamo a scaricare tre file in modo asincrono. Per semplicità — simuleremo il download con una delay (Task.Delay). Gli esempi sono realistici e il codice è funzionante.
using System;
using System.Threading.Tasks;
class Program
{
// Simuliamo il download di un file con una delay, ritorniamo il nome
static async Task<string> DownloadFileAsync(string fileName)
{
Console.WriteLine($"► Inizio download: {fileName}");
await Task.Delay(2000); // Aspettiamo 2 secondi
Console.WriteLine($"✓ Download completato: {fileName}");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
// Avviamo tutti i download contemporaneamente
var downloadTasks = new Task<string>[files.Length];
for (int i = 0; i < files.Length; i++)
{
downloadTasks[i] = DownloadFileAsync(files[i]);
}
// Aspettiamo che tutti i download siano completati
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"Tutti i download sono completati! Elenco: {string.Join(", ", results)}");
}
}
Cosa succede qui?
- Non aspettiamo ogni task una alla volta. Tutti e tre partono in parallelo (asincroni).
- Task.WhenAll(downloadTasks) restituisce una task che si completerà solo quando tutte le task saranno completate.
- Dopo possiamo lavorare con i risultati di tutte le task — usarli, stamparli, inviarli oltre.
Analogia
È come se avessi incaricato tre corrieri di consegnare tre pacchi e volessi avvisare il capo solo quando tutti sono arrivati a destinazione con successo.
Schema di funzionamento di Task.WhenAll
sequenceDiagram
participant Program
participant Task1
participant Task2
participant Task3
Program->>Task1: Avvio
Program->>Task2: Avvio
Program->>Task3: Avvio
Task1-->>Program: Completata (può finire prima delle altre)
Task2-->>Program: Completata
Task3-->>Program: Completata
Program->>Program: Task.WhenAll completata, tutti i risultati sono disponibili
Cosa succede se una delle task termina con un errore?
Task.WhenAll non si interrompe se una task ha generato un'eccezione. Aspetta la fine di tutte le task. Se almeno una ha sollevato un'eccezione — la task risultante sarà nello stato Faulted, e conterrà la collezione di tutte le eccezioni che si sono verificate.
Esempio
static async Task<string> MayThrowAsync(string fileName)
{
await Task.Delay(500);
if (fileName == "fileB.txt")
throw new Exception("Errore nel download di fileB.txt");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
var downloadTasks = files.Select(MayThrowAsync).ToArray();
try
{
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"Tutto ok: {string.Join(", ", results)}");
}
catch (Exception ex)
{
Console.WriteLine($"Almeno una task è terminata con errore: {ex.Message}");
}
}
In caso di eccezione, per vedere tutti gli errori interni usa AggregateException.InnerExceptions.
Documentazione ufficiale: Task.WhenAll docs
2. Task.WhenAny: aspettare la prima task che termina
Task.WhenAny è anch'esso un metodo statico, ma si completerà non appena una qualsiasi delle task passate passerà allo stato "Completata" (sia con successo che con errore). Restituisce il riferimento alla prima task che si è completata.
Firma
Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)
Analogia
Chi finisce di preparare la torta per primo — è il fortunato, gli altri possono fermarsi. A volte basta sapere quale server ha risposto per primo e usare il suo risultato.
Esempio: chi è il più veloce?
using System;
using System.Threading.Tasks;
class Program
{
// Simuliamo una richiesta con velocità diversa
static async Task<string> RequestAsync(string name, int delay)
{
await Task.Delay(delay);
return $"{name} è terminato in {delay} ms";
}
static async Task Main()
{
var taskA = RequestAsync("A", 1000);
var taskB = RequestAsync("B", 700); // Il più veloce
var taskC = RequestAsync("C", 1500);
// Aspettiamo la prima task che si completa
Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);
Console.WriteLine($"Il primo a terminare è stato: {finished.Result}");
}
}
Con WhenAny scopriamo quale delle task è arrivata prima al traguardo. Dopo possiamo cancellare le altre task, per esempio usando CancellationToken (argomento delle lezioni successive).
Perché WhenAny restituisce la task stessa e non il risultato?
Perché il metodo non sa quale tipo di risultato avrà la task che terminerà per prima: non può sapere in anticipo quale task finirà. Per questo restituisce la task — poi dobbiamo estrarre il risultato noi stessi: con await o con la proprietà Result di quella task.
WhenAll vs WhenAny: illustrazione comparativa
| Metodo | Quando scatta | Cosa restituisce | Scenario tipico |
|---|---|---|---|
|
Quando tutte sono completate | Una task (Task/Task<T[]>) | Aspettiamo tutte le risposte o tutti i file, aggregiamo i risultati |
|
Quando una qualsiasi task si completa | La task che si è completata per prima | Usiamo il primo risultato, cancelliamo le altre |
3. Combiniamo WhenAll e WhenAny — scenari pratici
A volte serve prima aspettare una qualsiasi task e poi tutte. O viceversa.
Esempio: "Ping-pong" tra due server
Immagina di inviare richieste a due server-clone nel caso uno rallenti. Ma usi il risultato del primo che risponde.
static async Task Main()
{
var fastServer = RequestAsync("Server veloce", 400); // 400 ms
var slowServer = RequestAsync("Server lento", 2000); // 2000 ms
var completed = await Task.WhenAny(fastServer, slowServer);
Console.WriteLine(await completed);
// *Opzionale*: qui puoi cancellare le task non ancora completate tramite CancellationToken
}
Esempio: avviare l'elaborazione solo dopo che tutti i dati sono scaricati
La tua applicazione ha tre fonti di dati. Solo quando tutte e tre sono scaricate ha senso passare all'elaborazione:
static async Task Main()
{
var task1 = DownloadFileAsync("a.txt");
var task2 = DownloadFileAsync("b.txt");
var task3 = DownloadFileAsync("c.txt");
var all = await Task.WhenAll(task1, task2, task3);
ProcessFiles(all[0], all[1], all[2]);
}
4. Errori tipici quando si lavora con Task.WhenAll e Task.WhenAny
Errore №1: aspettarsi che WhenAll si completi alla prima task.
I principianti pensano che Task.WhenAll si completi quando finisce la prima task. In realtà aspetta il completamento di tutte le task.
Errore №2: ignorare le eccezioni in WhenAll.
Se una delle task lancia un'eccezione, la task risultante sarà nello stato Faulted. Se non gestisci AggregateException.InnerExceptions, puoi perdere errori importanti.
Errore №3: accedere a Result senza controllare in WhenAny.
Se la prima task completata è fallita, chiamare Result lancerà l'eccezione. Controlla Task.IsFaulted prima di accedere.
Errore №4: eseguire le task in sequenza invece che in parallelo.
Per esempio, aspettare ogni task con await dentro un ciclo invece di usare Task.WhenAll riduce drasticamente le prestazioni.
GO TO FULL VERSION