CodeGym /Kurse /C# SELF /Klassen Task und <...

Klassen Task und Task<TResult>

C# SELF
Level 60 , Lektion 0
Verfügbar

1. Einführung

In der Welt der asynchronen Programmierung leben wir nach dem Prinzip "Delegation mit Rückmeldung". Stell dir vor: du musst eine riesige Datei herunterladen, Gigabytes an Logs analysieren oder eine Anfrage an einen entfernten Server senden. Anstatt regungslos zu warten wie eine Statue, sagen wir dem System: "Mach das für mich, ich beschäftige mich solange mit etwas anderem. Wenn du fertig bist — sag mir Bescheid!"

Genau hier tritt Task auf die Bühne — die elegante Umsetzung dieses Ansatzes. Das ist nicht nur eine technische Abstraktion, sondern eine Art "smarter Vermittler", der die Verantwortung für die Ausführung übernimmt und sicherstellt, dass das Ergebnis nicht in der digitalen Leere verschwindet.

Task funktioniert wie ein persönlicher Assistent, dem du eine wichtige Aufgabe anvertraust. Er nickt, schreibt die Aufgabe in sein Notizbuch und sagt: "Geh ruhig deinen Dingen nach, ich melde mich, wenn alles fertig ist". Und tatsächlich meldet er sich — mit dem Ergebnis in der Hand oder einer ehrlichen Erklärung, warum etwas schief gelaufen ist.

Wenn man eine alltagsnahe Analogie sucht, erinnert Task an ein modernes Terminbuchungssystem: du meldest dich online für einen Arzttermin an, erhältst eine Bestätigung und musst nicht stundenlang in der Schlange sitzen. Das System erinnert dich rechtzeitig, und du kannst dein Leben weiterleben.

Klasse Task

Task ist der grundlegende Baustein asynchroner Programmierung in .NET. Er repräsentiert eine gestartete oder zukünftige Operation, deren Ergebnis später verfügbar sein wird. Wenn eine Methode nichts zurückgeben soll, verwenden wir einfach Task.

public async Task BackupToCloudAsync()
{
    // Macht das Backup-Zauberwerk, gibt nichts zurück
}

Klasse Task<TResult>

Wenn man ein Ergebnis zurückgeben muss (z.B. einen string, eine Zahl, ein Objekt…), verwendet man Task<TResult>:

public async Task<string> DownloadHtmlAsync(string url)
{
    // Lädt die Seite herunter und gibt den HTML-Code zurück
    return "<html>...</html>";
}

Warum Task und nicht Thread?

Thread verwaltet den eigentlichen Thread (das ist schwer und fehleranfällig), während Task eine höherstufige Abstraktion ist: er kann im Threadpool laufen, asynchron ohne neuen Thread arbeiten (z.B. bei I/O-Operationen) und erfordert nicht, dass du dich um niedrigstufige Details kümmerst.

Die Klasse Task erlaubt es zu beschreiben: "Ich möchte diese Aktion starten", und wie genau sie ausgeführt wird — das überlässt man .NET!

2. Aufbau eines Task-Objekts

Eigenschaften und Methoden von Task, die man kennen sollte

Eigenschaft / Methode Beschreibung
Status
Aktueller Zustand der Task
Result
Ergebnis für Task<TResult> (blockiert den Thread)
IsCompleted
Ob die Task abgeschlossen ist
IsFaulted
Ob eine Ausnahme in der Task aufgetreten ist
IsCanceled
Ob die Task abgebrochen wurde
Wait()
Blockiert den aktuellen Thread bis zum Abschluss (gefährlich)
ContinueWith()
Eine weitere Aufgabe nach Abschluss starten
Exception
Zugriff auf die Ausnahme, falls die Task mit Fehler beendet wurde
Id
Einzigartige Kennung der Task

Wie eine asynchrone Methode mit Task funktioniert

sequenceDiagram
    participant Main as Main Thread
    participant Task as Task (Hintergrundaufgabe)
    Main->>Task: Start Task.Run(() => ...)
    Note right of Task: Ausführung im Hintergrund
(CPU oder I/O) alt Aufgabe abgeschlossen Task->>Main: await beendet, wir machen weiter else Fehler Task->>Main: await wirft eine Ausnahme end

3. Erstellen und Starten von Tasks: wie Task arbeitet

Asynchrone Methoden mit async

Der häufigste Fall — du deklarierst die Methode als async und gibst entweder Task oder Task<TResult> zurück (wie wir gerade gesehen haben).

Task.Run: Ausführung im Threadpool

Wenn du schwere Arbeit im Hintergrund erledigen musst (z.B. große Berechnungen oder Video-Codierung), kannst du Task.Run verwenden:

Task work = Task.Run(() =>
{
    // Aufwändige Berechnungen — blockiere nicht den Hauptthread!
    Console.WriteLine("Hintergrundberechnungen gestartet...");
    Thread.Sleep(2000); // Emulation einer langen Arbeit
    Console.WriteLine("Hintergrundberechnungen beendet!");
});

Wenn es nützlich ist, ein Ergebnis zu erhalten:

Task<int> calculateTask = Task.Run(() =>
{
    // Zum Beispiel die Summe der ersten 100 Zahlen berechnen
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

Das ist eine niedrigere und flexiblere Methode, erlaubt feine Einstellungen beim Starten der Task (z.B. Scheduler, Parameterweitergabe usw.). In modernem Code wird meist empfohlen, Task.Run zu verwenden, weil es einfacher ist und vor Fehlern schützt.

4. Anwendung des Tages: unser Buchverzeichnis

Angenommen, wir haben eine Anwendung für ein Buchverzeichnis und müssen eine Funktion hinzufügen, die Bücher aus einer "Cloud"-Quelle lädt — das ist eine I/O-bound Operation (langsame HTTP-Anfrage oder Lesen aus einer Datei).

Fügen wir eine Methode hinzu, die asynchron Bücher "lädt" (wir emulieren die Verzögerung):

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("Lade Bücher...");
        await Task.Delay(2000); // Emulation langer Ladezeit (z.B. HTTP oder Datei)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("Bücher erfolgreich geladen.");
    }
}

In Main rufen wir das asynchrone Laden auf (mit await):

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"Im Katalog sind {catalog.Books.Count} Bücher.");

Tabelle: Hauptarten, Task zu erstellen und zu starten

Erstellungsweise Anwendung Ergebnis Einsatzgebiet
async-Methode
async Task / async Task<T>
Asynchrone Operation Meist I/O, bequem
Task.Run
Task.Run(() => { ... })
Hintergrundaufgabe CPU-bound (Berechnungen)
TaskCompletionSource<T>
Task manuell erstellen und abschließen Volle Kontrolle für den Programmierer Selten, für low-level Sachen

5. Lebenszyklus einer Task

Eine Task kann sich in verschiedenen Zuständen befinden:

  • Created — die Aufgabe ist erstellt, aber nicht gestartet (bei explizitem Start).
  • WaitingToRun — wartet in der Queue des Threadpools.
  • Running — läuft gerade.
  • WaitingForActivation — wartet auf Start oder externe Aktivierung.
  • RanToCompletion — erfolgreich abgeschlossen.
  • Faulted — mit Fehler (Ausnahme) beendet.
  • Canceled — abgebrochen (wenn Cancellation unterstützt wird).

Diagramm

flowchart LR
    Start -->|Task starten| Running
    Running -->|Erfolgreich| Completed
    Running -->|Fehler| Faulted
    Running -->|Abbruch| Canceled

Das prüfen wir praktisch

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // Normalerweise: Running oder WaitingToRun
await task;
Console.WriteLine(task.Status); // RanToCompletion nach Abschluss

6. Wie bekommt man das Ergebnis aus Task<TResult>?

Task<TResult> ist eine Hülle um ein Ergebnis, das in der Zukunft entstehen wird. Wenn man auf das Ergebnis warten muss, benutzt man await:

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

Wenn man vergisst await zu schreiben, erhält man eine Task (ein Versprechen) statt des Ergebnisses. Das ist eine typische "asynchrone Falle".

Alternative: synchrones Ergebnis holen (NICHT IM UI TUN!)

Manchmal (z.B. in Tests) muss man das Ergebnis ohne await bekommen. Man kann die Eigenschaft .Result verwenden:

int result = sumTask.Result;

Aber wenn die Task noch nicht abgeschlossen ist, blockiert dieser Code den Thread, und wenn das der UI-Thread ist, friert die Anwendung ein! Deshalb: besser immer await bevorzugen.

Typische Fehler mit Task und Task<TResult>

Vergessen, Task zurückzugeben — die Methode wurde void. Wenn die Methode keinen Rückgabewert hat — gib Task zurück, aber nicht void, sonst kann man Fehler nicht behandeln.

Ignorieren von await. Einfach die Methode aufgerufen, ohne auf das Ergebnis zu warten, und die Task lebt ihr eigenes Leben ("fire and forget"). Du weißt nicht mehr, wann sie fertig ist oder abgestürzt ist.

Blockierendes Warten via .Result oder .Wait(). Deadlocks sind leicht möglich, besonders im UI und in ASP.NET. Verwende stattdessen nur await.

7. Fortgeschrittene Möglichkeiten von Task

Task-Kette: ContinueWith

Man kann Aktionen "anhängen", die nach Abschluss einer Task ausgeführt werden sollen, mittels ContinueWith:

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"Fertig! Ergebnis: {t.Result}");
    });

In modernem C# macht man das aber meist mit async/await — das ist lesbarer.

Beispiel: paralleles und sequentielles Laden von Daten

Angenommen, du musst zwei Bücher aus verschiedenen Quellen laden. Du kannst beide Tasks parallel starten und auf beide warten:

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // Warten auf beide Tasks parallel
    await Task.WhenAll(t1, t2);

    // Ergebnisse zusammenführen
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "Cloud"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "Lokale Festplatte"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

Beachte: mit await Task.WhenAll(...) starten beide Anfragen gleichzeitig und laufen parallel (sofern möglich), und die Anwendung wartet, bis beide abgeschlossen sind.

8. Nützliche Nuancen

Task und Fire-and-forget

Manchmal möchte man eine Task starten und nicht darauf warten (z.B. Logs in die Cloud schicken oder einen Toast anzeigen, während der Benutzer arbeitet):

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // Langes Senden des Logs
        Thread.Sleep(1000);
        Console.WriteLine($"Log gesendet: {message}");
    });
}

Aber denk dran: wenn in so einer Task ein Fehler auftritt — wird es schwierig, das herauszufinden. Wenn möglich, gib lieber Task zurück und logge zumindest Ausnahmen intern!

Task und Task<TResult> im echten Leben

  • In Client-Apps (UWP/WPF/WinForms) blockiere nicht das UI — benutze Task für lange Operationen (Dateien, Netzwerk).
  • In WebAPI/ASP.NET hilft Task, Threads nicht mit Wartezeiten auf Netzwerk/DB zu belegen, was die Performance erhöht.
  • Organisiere parallele Ausführung: gleichzeitig herunterladen, verarbeiten und speichern.
  • Fast alle langen Methoden haben Async-Varianten: File.ReadAllTextAsync, HttpClient.GetStringAsync usw.

FAQ und unerwartete Aspekte

Frage: Warum wird Task manchmal synchron ausgeführt?
Antwort: Wenn die Operation bereits abgeschlossen ist (z.B. Ergebnis gecached), kann der Compiler und/oder Scheduler die Methode im selben Thread synchron abschließen. Das ist normal und beschleunigt wiederholte Aufrufe.

Frage: Warum sollte man nicht async void verwenden?
Antwort: Solche Methoden kann man nicht warten, man kann ihre Fehler nicht abfangen und ihr Abschluss lässt sich nicht verfolgen. Verwende Task, und async void nur für EventHandler (z.B. Button_Click).

Frage: Kann man mehrere Tasks starten und nur auf eine warten?
Antwort: Ja — verwende Task.WhenAny.

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