1. Task.WhenAll: poczekać na wszystkie naraz
W prawdziwym życiu rzadko zdarza się, że robisz tylko jedną rzecz. Wstajesz z łóżka i przy tym patrzysz na zegarek, bierzesz prysznic i śpiewasz, ubierasz się i słuchasz muzyki, jednocześnie robiąc kawę. Albo pracując przy komputerze, jednocześnie ściągasz kilka plików, wysyłasz paczkę zapytań sieciowych, obliczasz masę danych dla różnych kierunków. Wszystkie te czynności mogą (i powinny!) działać równolegle, żeby nie tracić czasu użytkownika. Nie chcemy czekać, aż każde zadanie wykona się jedno po drugim! Jak zorganizować taki "orkiestr"? Jak zrozumieć, kiedy WSZYSTKO się zakończyło? Albo przeciwnie, dowiedzieć się, kto pierwszy poradził sobie?
Task.WhenAll i Task.WhenAny — to narzędzia właśnie do takich scenariuszy.
Metoda WhenAll
Task.WhenAll — to metoda statyczna klasy Task, która przyjmuje kolekcję zadań i zwraca nowe zadanie kończące się tylko wtedy, gdy zakończą się wszystkie przekazane zadania. To jakbyś na egzaminie czekał, aż wszyscy współstudenci oddadzą bilety, zanim wyjdziesz z sali.
Sygantura metody
Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)
Są wersje dla zadań zwracających wyniki (Task<TResult>) i "pustych" (Task).
Najprostszy przykład: czekać na pobranie wszystkich plików
Zróbmy pobieranie trzech plików asynchronicznie. Dla prostoty — symulujemy pobieranie opóźnieniem (Task.Delay). Przykłady są realne, kod działa.
using System;
using System.Threading.Tasks;
class Program
{
// Symulujemy pobieranie pliku z opóźnieniem, zwracamy nazwę
static async Task<string> DownloadFileAsync(string fileName)
{
Console.WriteLine($"► Zaczynam pobieranie: {fileName}");
await Task.Delay(2000); // Czekamy 2 sekundy
Console.WriteLine($"✓ Pobieranie zakończone: {fileName}");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
// Uruchamiamy wszystkie pobrania jednocześnie
var downloadTasks = new Task<string>[files.Length];
for (int i = 0; i < files.Length; i++)
{
downloadTasks[i] = DownloadFileAsync(files[i]);
}
// Oczekujemy zakończenia wszystkich pobrań
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"Wszystkie pobrania zakończone! Lista: {string.Join(", ", results)}");
}
}
Co tu się dzieje?
- Nie czekamy na każde zadanie po kolei. Wszystkie trzy uruchamiają się równolegle (asynchronicznie).
- Task.WhenAll(downloadTasks) zwraca zadanie, które zakończy się dopiero, gdy wszystkie zadania się zakończą.
- Potem możemy pracować z wynikami wszystkich zadań — użyć ich, wypisać na ekran, przesłać dalej.
Anologia
To jakbyś zlecił trzem kurierom dostarczenie trzech paczek i chcesz poinformować szefa dopiero wtedy, gdy wszyscy dotrą do klientów.
Schemat działania Task.WhenAll
sequenceDiagram
participant Program
participant Zadanie1
participant Zadanie2
participant Zadanie3
Program->>Zadanie1: Uruchomienie
Program->>Zadanie2: Uruchomienie
Program->>Zadanie3: Uruchomienie
Zadanie1-->>Program: Zakończone (może być wcześniej niż pozostałe)
Zadanie2-->>Program: Zakończone
Zadanie3-->>Program: Zakończone
Program->>Program: Task.WhenAll zakończony, wszystkie wyniki dostępne
Co jeśli jedno z zadań zakończy się błędem?
Task.WhenAll nie przerywa czekania, gdy któreś zadanie zakończy się błędem. Czeka na zakończenie wszystkich zadań. Jeśli przynajmniej jedno rzuciło wyjątek — wynikowe zadanie będzie w stanie Faulted, i w nim będzie kolekcja wszystkich wyjątków, które wystąpiły.
Przykład
static async Task<string> MayThrowAsync(string fileName)
{
await Task.Delay(500);
if (fileName == "fileB.txt")
throw new Exception("Błąd pobierania 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($"Wszystko ok: {string.Join(", ", results)}");
}
catch (Exception ex)
{
Console.WriteLine($"Przynajmniej jedno zadanie zakończyło się błędem: {ex.Message}");
}
}
W przypadku wyjątku, żeby zobaczyć wszystkie wewnętrzne błędy, użyj AggregateException.InnerExceptions.
Oficjalna dokumentacja: Task.WhenAll docs
2. Task.WhenAny: poczekać na pierwsze zakończone zadanie
Task.WhenAny — też metoda statyczna, ale zakończy się, jak tylko dowolne z przekazanych zadań przejdzie w stan "Zakończone" (nieistotne, czy z błędem czy pomyślnie). Zwraca referencję do pierwszego zakończonego zadania.
Sygantura
Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)
Anologia
Kto pierwszy upiecze ciasto — ten jest bohaterem, a resztę można poprosić, żeby przestali. Czasami wystarczy wiedzieć, który z kilku serwerów odpowiedział najszybciej i użyć właśnie jego wyniku.
Przykład: kto jest szybszy?
using System;
using System.Threading.Tasks;
class Program
{
// Symulujemy zapytanie o różnej prędkości
static async Task<string> RequestAsync(string name, int delay)
{
await Task.Delay(delay);
return $"{name} zakończył się w {delay} ms";
}
static async Task Main()
{
var taskA = RequestAsync("A", 1000);
var taskB = RequestAsync("B", 700); // Najszybszy
var taskC = RequestAsync("C", 1500);
// Czekamy na pierwsze zakończone zadanie
Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);
Console.WriteLine($"Jako pierwszy zakończył się: {finished.Result}");
}
}
Dzięki WhenAny dowiadujemy się, które zadanie dotarło do mety pierwsze. Potem można anulować pozostałe zadania, np. przez CancellationToken (temat kolejnych wykładów).
Dlaczego WhenAny zwraca właśnie zadanie, a nie wynik?
Bo metoda nie wie, jaki typ wyniku ma zadanie: nie może z góry wiedzieć, które zadanie zakończy się pierwsze. Dlatego zwraca samo zadanie — dalej musimy sami wyciągnąć wynik: przez await albo właściwość Result tego zadania.
WhenAll vs WhenAny: porównawcza ilustracja
| Metoda | Kiedy zadziała | Co zwraca | Typowy scenariusz |
|---|---|---|---|
|
Kiedy wszystkie się zakończą | Zadanie (Task/Task<T[]>) | Czekamy na wszystkie odpowiedzi lub wszystkie pliki, agregujemy wyniki |
|
Kiedy któreś z zadań się zakończy | Zadanie pierwszego zakończonego zadania | Używamy pierwszego wyniku, anulujemy pozostałe |
3. Łączymy WhenAll i WhenAny — życiowe scenariusze
Czasami trzeba najpierw poczekać na dowolne zadanie, a potem na wszystkie. Albo odwrotnie.
Przykład: "ping-pong" między dwoma serwerami
Wyobraź sobie, że wysyłasz zapytania jednocześnie do dwóch klonów serwera na wypadek, gdyby jeden zwalniał. Ale wynik używasz pierwszego.
static async Task Main()
{
var fastServer = RequestAsync("Szybki serwer", 400); // 400 ms
var slowServer = RequestAsync("Wolny serwer", 2000); // 2000 ms
var completed = await Task.WhenAny(fastServer, slowServer);
Console.WriteLine(await completed);
// *Opcjonalnie*: tutaj można anulować jeszcze niezakończone zadania przez CancellationToken
}
Przykład: Uruchomić przetwarzanie dopiero po pobraniu wszystkich danych
W twojej aplikacji są trzy źródła danych. Dopiero gdy wszystkie trzy się pobiorą, warto przejść do przetwarzania:
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. Typowe błędy przy pracy z Task.WhenAll i Task.WhenAny
Błąd nr 1: oczekiwanie zakończenia pierwszego zadania przy WhenAll.
Nowicjusze myślą, że Task.WhenAll zakończy się, jak tylko wykona się pierwsze zadanie. W rzeczywistości czeka on na zakończenie wszystkich zadań.
Błąd nr 2: ignorowanie wyjątków w WhenAll.
Jeśli jedno z zadań rzuci wyjątek, wynikowe zadanie będzie w stanie Faulted. Nie obsłużenie AggregateException.InnerExceptions może spowodować pominięcie ważnych błędów.
Błąd nr 3: dostęp do Result bez sprawdzenia przy WhenAny.
Jeśli pierwsze zakończone zadanie zakończyło się błędem, wywołanie Result rzuci wyjątek. Sprawdzaj Task.IsFaulted przed dostępem.
Błąd nr 4: uruchamianie zadań sekwencyjnie zamiast równolegle.
Na przykład oczekiwanie każdego zadania przez await w pętli zamiast użycia Task.WhenAll znacząco obniża wydajność.
GO TO FULL VERSION