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 |
|---|---|
|
Aktueller Zustand der Task |
|
Ergebnis für Task<TResult> (blockiert den Thread) |
|
Ob die Task abgeschlossen ist |
|
Ob eine Ausnahme in der Task aufgetreten ist |
|
Ob die Task abgebrochen wurde |
|
Blockiert den aktuellen Thread bis zum Abschluss (gefährlich) |
|
Eine weitere Aufgabe nach Abschluss starten |
|
Zugriff auf die Ausnahme, falls die Task mit Fehler beendet wurde |
|
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 | |
Asynchrone Operation | Meist I/O, bequem |
|
|
Hintergrundaufgabe | CPU-bound (Berechnungen) |
|
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.
GO TO FULL VERSION