1. Task.WhenAll: auf alle gleichzeitig warten
Im echten Leben passiert selten, dass man nur eine Sache macht. Du stehst auf und schaust auf die Uhr, duschst und summst, ziehst dich an und hörst Musik, während du dir Kaffee machst. Oder am Computer lädst du mehrere Dateien hoch, schickst mehrere Netzwerk-Requests und berechnest Daten in verschiedenen Richtungen. All diese Aktionen können (und sollten!) parallel laufen, damit man die Zeit des Nutzers nicht verschwendet. Wir wollen nicht warten, bis jede Aufgabe nacheinander fertig ist! Wie organisiert man so ein „Orchester“? Wie weiß man, wann ALLES fertig ist? Oder im Gegenteil, wer als erstes fertig ist?
Task.WhenAll und Task.WhenAny sind die Werkzeuge für solche Szenarien.
Methode WhenAll
Task.WhenAll ist eine statische Methode der Klasse Task, die eine Sammlung von Tasks entgegennimmt und eine neue Task zurückgibt, die erst abgeschlossen ist, wenn alle übergebenen Tasks abgeschlossen sind. Das ist, als würdest du bei einer Prüfung warten, bis alle Kommilitonen ihre Prüfungsbögen abgegeben haben, bevor du den Raum verlässt.
Methodensignatur
Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)
Es gibt Versionen für Tasks, die Ergebnisse zurückgeben (Task<TResult>) und für „leere“ Tasks (Task).
Einfachstes Beispiel: auf das Laden aller Dateien warten
Lass uns drei Dateien asynchron laden. Zur Vereinfachung simulieren wir das Laden mit einer Verzögerung (Task.Delay). Die Beispiele sind realistisch und lauffähig.
using System;
using System.Threading.Tasks;
class Program
{
// Wir simulieren das Herunterladen einer Datei mit Verzögerung, geben den Namen zurück
static async Task<string> DownloadFileAsync(string fileName)
{
Console.WriteLine($"► Beginne Download: {fileName}");
await Task.Delay(2000); // Warten 2 Sekunden
Console.WriteLine($"✓ Download abgeschlossen: {fileName}");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
// Starte alle Downloads gleichzeitig
var downloadTasks = new Task<string>[files.Length];
for (int i = 0; i < files.Length; i++)
{
downloadTasks[i] = DownloadFileAsync(files[i]);
}
// Warten auf das Ende aller Downloads
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"Alle Downloads abgeschlossen! Liste: {string.Join(", ", results)}");
}
}
Was passiert hier?
- Wir warten nicht auf jede Task nacheinander. Alle drei starten parallel (asynchron).
- Task.WhenAll(downloadTasks) gibt eine Task zurück, die erst abgeschlossen wird, wenn alle Tasks fertig sind.
- Danach können wir mit den Ergebnissen aller Tasks arbeiten — sie verwenden, ausgeben oder weiterreichen.
Analogie
Das ist so, als hättest du drei Kuriere beauftragt, drei Pakete zu liefern, und du willst deinen Chef erst informieren, wenn alle erfolgreich angekommen sind.
Ablaufdiagramm von Task.WhenAll
sequenceDiagram
participant Program
participant Aufgabe1
participant Aufgabe2
participant Aufgabe3
Program->>Aufgabe1: Start
Program->>Aufgabe2: Start
Program->>Aufgabe3: Start
Aufgabe1-->>Program: Abgeschlossen (kann früher sein)
Aufgabe2-->>Program: Abgeschlossen
Aufgabe3-->>Program: Abgeschlossen
Program->>Program: Task.WhenAll abgeschlossen, alle Ergebnisse verfügbar
Was, wenn eine der Tasks mit einem Fehler endet?
Task.WhenAll bricht nicht sofort ab, wenn eine Task mit Fehler endet. Er wartet auf das Ende aller Tasks. Wenn mindestens eine Task eine Ausnahme wirft, ist die zusammengefasste Task im Zustand Faulted und enthält eine Sammlung aller aufgetretenen Ausnahmen.
Beispiel
static async Task<string> MayThrowAsync(string fileName)
{
await Task.Delay(500);
if (fileName == "fileB.txt")
throw new Exception("Fehler beim Herunterladen von 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($"Alles gut: {string.Join(", ", results)}");
}
catch (Exception ex)
{
Console.WriteLine($"Mindestens eine Task ist mit einem Fehler fertig geworden: {ex.Message}");
}
}
Im Fehlerfall kannst du alle inneren Fehler anschauen über AggregateException.InnerExceptions.
Offizielle Doku: Task.WhenAll docs
2. Task.WhenAny: auf die zuerst fertig werdende Task warten
Task.WhenAny ist ebenfalls eine statische Methode, aber sie wird abgeschlossen, sobald eine der übergebenen Tasks in den Zustand „abgeschlossen“ geht (egal ob mit Fehler oder erfolgreich). Sie gibt eine Referenz auf die zuerst abgeschlossene Task zurück.
Signatur
Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)
Analogie
Wer als Erstes den Kuchen fertigbackt, ist der Gewinner — die anderen kann man stoppen. Manchmal reicht es, zu wissen, welcher von mehreren Servern zuerst geantwortet hat und genau dessen Ergebnis zu verwenden.
Beispiel: Wer ist schneller?
using System;
using System.Threading.Tasks;
class Program
{
// Wir simulieren eine Anfrage mit unterschiedlicher Geschwindigkeit
static async Task<string> RequestAsync(string name, int delay)
{
await Task.Delay(delay);
return $"{name} wurde in {delay} ms fertig";
}
static async Task Main()
{
var taskA = RequestAsync("A", 1000);
var taskB = RequestAsync("B", 700); // Am schnellsten
var taskC = RequestAsync("C", 1500);
// Warten auf die erste abgeschlossene Task
Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);
Console.WriteLine($"Als erstes fertig: {finished.Result}");
}
}
Mit WhenAny finden wir heraus, welche Task zuerst ins Ziel kommt. Danach kann man die übrigen Tasks abbrechen, z.B. mit einem CancellationToken (Thema für spätere Vorlesungen).
Warum WhenAny die Task selbst zurückgibt und nicht das Ergebnis?
Weil die Methode nicht wissen kann, welchen Ergebnis-Typ die Tasks haben: sie weiß nicht vorher, welche Task zuerst fertig ist. Deshalb gibt sie die Task zurück — und weiter musst du das Ergebnis selbst holen: per await oder über die Eigenschaft Result dieser Task.
WhenAll vs WhenAny: vergleichende Übersicht
| Methode | Wann ausgelöst | Was zurückgegeben wird | Typisches Szenario |
|---|---|---|---|
|
Wenn alle abgeschlossen sind | Task (Task/Task<T[]>) | Warten auf alle Antworten oder alle Dateien, Ergebnisse aggregieren |
|
Wenn irgendeine Task abgeschlossen ist | Die Task, die zuerst abgeschlossen wurde | Erstes Ergebnis nutzen, die anderen abbrechen |
3. Kombination von WhenAll und WhenAny — reale Szenarien
Manchmal willst du zuerst auf irgendeine Task warten und dann auf alle. Oder umgekehrt.
Beispiel: „Ping-Pong“ zwischen zwei Servern
Stell dir vor, du schickst Requests gleichzeitig an zwei Server-Klone, falls einer träge ist. Aber du verwendest das Ergebnis desjenigen, der zuerst antwortet.
static async Task Main()
{
var fastServer = RequestAsync("Schneller Server", 400); // 400 ms
var slowServer = RequestAsync("Langsamer Server", 2000); // 2000 ms
var completed = await Task.WhenAny(fastServer, slowServer);
Console.WriteLine(await completed);
// *Optional*: Hier kann man die noch nicht fertigen Tasks mit CancellationToken abbrechen
}
Beispiel: Verarbeitung erst starten, wenn alle Daten geladen sind
In deiner App gibt es drei Datenquellen. Erst wenn alle drei geladen sind, solltest du mit der Verarbeitung beginnen:
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. Typische Fehler bei der Arbeit mit Task.WhenAll und Task.WhenAny
Fehler Nr.1: erwarten, dass Task.WhenAll bei der ersten fertigen Task ausgelöst wird.
Anfänger denken, dass Task.WhenAll fertig ist, sobald die erste Task abgeschlossen ist. Tatsächlich wartet er auf alle Tasks.
Fehler Nr.2: Ausnahmen in WhenAll ignorieren.
Wenn eine Task eine Ausnahme wirft, ist die zusammengefasste Task im Zustand Faulted. Wenn man AggregateException.InnerExceptions nicht beachtet, übersieht man wichtige Fehler.
Fehler Nr.3: Zugriff auf Result ohne Prüfung bei WhenAny.
Wenn die zuerst abgeschlossene Task mit einem Fehler endet, wirft der Zugriff auf Result eine Ausnahme. Prüfe Task.IsFaulted bevor du darauf zugreifst.
Fehler Nr.4: Tasks nacheinander statt parallel starten.
Zum Beispiel jede Task in einer Schleife mit await zu warten statt Task.WhenAll zu benutzen, reduziert die Performance drastisch.
GO TO FULL VERSION