CodeGym /Kurse /C# SELF /Verwaltung mehrerer Aufgaben

Verwaltung mehrerer Aufgaben

C# SELF
Level 60 , Lektion 2
Verfügbar

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
Task.WhenAll
Wenn alle abgeschlossen sind Task (Task/Task<T[]>) Warten auf alle Antworten oder alle Dateien, Ergebnisse aggregieren
Task.WhenAny
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.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION