1. Einführung
Bevor wir in den Code springen, klären wir die Motivation und stellen uns eine typische Situation vor. Angenommen, dein Programm lädt eine Datei aus dem Internet herunter:
// Pseudo-code
var data = DownloadFile("https://example.com/file");
ProcessData(data);
Das Problem hier: während des Downloads "friert" das Programm ein. Keine anderen Aktionen werden ausgeführt — der Benutzer kann weder die Maus bewegen, noch Buttons klicken, noch einfach abwarten ohne ein trauriges "eingefrorenes" Fenster.
Früher (und in anderen Sprachen) musste man für dieses Problem Threads (Thread), Tasks (Task), Delegates, Timer benutzen — all das führte zu einem Wirrwarr von Code, der schwer zu lesen und zu warten ist. In C# hat das Team das Leben einfacher gemacht: es gibt Asynchronität mit den Schlüsselwörtern async und await. Jetzt kann man asynchronen Code fast so einfach wie synchronen schreiben.
Klassischer "asynchroner Schmerz" ohne async/await
Zum Kontrast schauen wir, wie eine lange Operation mit Threads aussehen würde, wenn wir die UI nicht blockieren wollen:
// Beispiel ohne async/await, manuell
var thread = new Thread(() =>
{
var data = DownloadFile("https://example.com/file");
Console.WriteLine("Datei heruntergeladen!");
});
thread.Start();
Dieser Weg ist ziemlich grob: man muss sich manuell um Threads kümmern, es gibt keinen einfachen Weg, auf das Ergebnis zu "warten", und Fehler zu behandeln ist kompliziert.
2. Asynchronität in C#: Syntax
Definition: was sind async und await?
async — ist ein Modifikator, der einer Methode verbietet langweilig zu sein und sie asynchron macht. Solche Methoden geben normalerweise entweder Task (oder Task<T>) oder ValueTask zurück. Das ist ein Versprechen, dass das Ergebnis kommt, aber etwas später (wie eine Bestellung im Online-Shop — bestellt und warten).
await — ist ein Operator, der sagt: "Stoppe an dieser Zeile, wenn du sie erreichst. Warte, bis die Operation abgeschlossen ist, aber blockiere den Thread nicht! Alles andere kann weiterlaufen."
Wie sieht eine asynchrone Methode aus?
public async Task MyAsyncMethod()
{
Console.WriteLine("Datei wird geladen...");
var data = await DownloadFileAsync("https://example.com/file"); // Wir warten asynchron auf das Ergebnis!
Console.WriteLine("Fertig!");
}
Achte auf:
- Der Methode wurde der Modifikator async hinzugefügt.
- Innerhalb der Methode wird await für die asynchrone Operation verwendet.
- Die Methode gibt Task zurück (oder Task<T>, falls ein Rückgabewert vorhanden ist).
Visualisierung: was passiert beim Aufruf einer async-Methode?
graph LR
A[Aufruf MyAsyncMethod] --> B[Abarbeitung bis await]
B --> C[Aufruf DownloadFileAsync]
C --> D{Warten}
D --> |Datei nicht geladen| E[Freigabe des Threads]
D --> |Datei geladen| F[Abarbeitung nach await]
F --> G[Beenden der Methode]
- Bis zum ersten await wird die Methode synchron ausgeführt.
- Beim await wird die Ausführung der Methode angehalten, die Kontrolle an den Aufrufer zurückgegeben.
- Wenn die asynchrone Operation abgeschlossen ist, geht die Ausführung nach dem await weiter — als wäre nichts gewesen.
3. Beispiel: asynchrones Laden mit async/await
Angenommen, unsere Übungsanwendung lädt jetzt Text aus dem Internet und druckt dessen Länge, ohne den übrigen Code zu blockieren.
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
// Asynchrone Methode (gibt Task zurück)
public static async Task DownloadAndPrintLengthAsync(string url)
{
Console.WriteLine("Beginne Download...");
// Wir verwenden HttpClient — er unterstützt asynchrone Methoden
using (var client = new HttpClient())
{
string data = await client.GetStringAsync(url);
Console.WriteLine($"Download fertig! Länge des Textes: {data.Length} Zeichen.");
}
Console.WriteLine("Methode beendet.");
}
static void Main()
{
// Starten der asynchronen Operation und warten auf deren Abschluss
var task = DownloadAndPrintLengthAsync("https://www.example.com");
task.Wait(); // Für einfache Konsolenbeispiele ist das akzeptabel. In echten UI- oder Web-Anwendungen führt dieser Aufruf zu Blockierung und möglichen Deadlocks.
}
}
Erklärungen:
- DownloadAndPrintLengthAsync — vollständig asynchron dank async und await.
- Innerhalb der Methode warten wir mit await auf das Ende des asynchronen String-Downloads.
- In Main() starten wir die Task und warten explizit mit Wait(). In modernem C# kann man auch Main asynchron machen (async Task Main) und einfach await benutzen.
4. Wie funktioniert das?
Unterschied zwischen synchronem und asynchronem Code
SYNCHRONER VARIANT
Console.WriteLine("Anfang");
string data = client.GetStringAsync(url).Result; // .Result blockiert den Thread!
Console.WriteLine("Operation abgeschlossen");
ASYNCHRONER VARIANT
Console.WriteLine("Anfang");
string data = await client.GetStringAsync(url); // Thread wird nicht blockiert
Console.WriteLine("Operation abgeschlossen");
Wie funktioniert await "unter der Haube"?
Wenn du await benutzt, "zerlegt" C# automatisch deine Methode in zwei (oder mehr) Teile: alles vor dem await und alles danach. Wenn die aufgerufene asynchrone Methode ein Task zurückgibt, kehrt deine Methode zum Aufrufer zurück, und sobald das Task abgeschlossen ist — die Ausführung geht nach dem await weiter. Das passiert automatisch; du musst dich nicht um Threads und Kontextwechsel manuell kümmern.
Interessante Tatsache: C# verwandelt deine asynchrone Methode in eine Zustandsmaschine, wobei jedes "await" eine neue "Rückkehrmarke" ist.
Verwendung von async/await in realen Anwendungen
static async Task Main(string[] args)
{
var downloadTask = DownloadAndPrintLengthAsync("https://www.example.com");
// Während der Download läuft, machen wir etwas anderes
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Arbeiten... Iteration {i}");
await Task.Delay(500); // Pause 0,5s, simuliert Arbeit
}
await downloadTask; // Warten auf das Ende des Downloads
}
Jetzt lädt das Programm etwas und blockiert nicht: "lebendige" Multitasking, fast wie eine Katze, die gleichzeitig schläft und die Futterschüssel im Blick behält.
Asynchronität ≠ Multithreading
Wichtiger Unterschied, den Anfänger oft verwechseln: asynchroner Code kann auf demselben Thread laufen! Asynchronität bedeutet, den Thread nicht zu blockieren, nicht unbedingt neue Threads zu erzeugen. OS-Threads sind teuer! Asynchronität erlaubt, den Thread freizugeben, damit er weiterarbeiten kann, statt auf lange Operationen (Netzwerk, Datei, Timer usw.) zu warten.
Vergleichstabelle: Wann was wählen?
| Szenario | Thread/Task | async/await |
|---|---|---|
| CPU-bound Aufgaben | Ja | Ja (durch Task.Run) |
| I/O-bound Aufgaben | Nicht effizient | Ideal |
| Viel Parallelität | Schwer | Einfach |
| Einfachheit des Codes | Hardcore | Leicht zu lesen |
5. Nützliche Feinheiten
Asynchrones Main
Mit modernen C#-Versionen kannst du auch die Methode Main asynchron machen!
static async Task Main(string[] args)
{
// Dein gesamter asynchroner Code kann direkt in Main await-et werden
await DownloadAndAnalyzeFileAsync("https://example.com/file");
}
Typischer Fehler: vergessenes await — die Task "läuft weg"
SomeAsyncFunction(); // nicht await-ed, niemand wartet auf das Ende!
Als Ergebnis läuft diese Task "ins Leere" — und wenn ein Fehler auftritt, wirst du ihn vielleicht nie bemerken!
Wann man keine async-Methoden schreiben sollte
- Wenn innerhalb der Methode keine asynchrone Operation (kein await) vorhanden ist, sollte sie nicht async sein.
- Vermeide Methoden mit async void (außer du bist ein Event-Handler).
Kurzregeln und typische Fragen
- Kann man await außerhalb einer async-Methode verwenden? Nein! Immer nur innerhalb von Methoden, die mit async markiert sind.
- Kann man mehrere await in einer Methode haben? Ja, so viele wie nötig — jede ist eine "Wartepunkt".
- Was, wenn man ein Ergebnis zurückgeben will? Verwende Task<T> und schreibe return.
- Kann man mehrere asynchrone Aufgaben kombinieren? Natürlich! Du kannst mehrere Tasks parallel starten und mit Task.WhenAll auf alle warten.
6. Typische Fehler bei der Nutzung von async/await
Fehler Nr.1: "Feuerball" — await fehlt nach dem Aufruf der asynchronen Task.
DownloadAndPrintLengthAsync("https://www.example.com");
Console.WriteLine("Alles erledigt!"); // Tatsächlich läuft der Download noch!
Der Code wartet NICHT auf das Ende der asynchronen Operation. Alle asynchronen Tasks müssen entweder await-ed werden oder explizit mit Wait() behandelt werden (aber letzteres ist gefährlich und kann zu Blockierungen führen).
Fehler Nr.2: Vermischung von synchronem und asynchronem Code über .Result oder .Wait().
Das ist ein Anti-Pattern, das alle Vorteile der Asynchronität zunichte macht. In UI- oder ASP.NET-Anwendungen führt das fast garantiert zu Deadlocks: die asynchrone Task wartet auf die Freigabe des Threads, aber der Thread ist blockiert und wartet auf die Task. Merke dir das Mantra: "async all the way" (Asynchronität bis ganz nach oben).
Fehler Nr.3: Verwendung von async void.
Methoden mit async void können nicht erwartet werden (await), und Ausnahmen, die aus ihnen geworfen werden, können nicht mit normalen try-catch abgefangen werden und führen meist zum Absturz der gesamten Anwendung. Die einzige legitime Verwendung von async void sind Event-Handler (z.B. async void Button_Click(...)), wenn die Signatur es verlangt. In allen anderen Fällen verwende async Task.
Fehler Nr.4: Überflüssiges async in einer Methode ohne await.
Wenn du eine Methode als async markierst, aber kein await darin verwendest, gibt der Compiler eine Warnung aus. Solcher Code wird komplett synchron ausgeführt, verursacht aber unnötigen Overhead durch die Erstellung einer "Zustandsmaschine". Das verwirrt und reduziert die Performance.
GO TO FULL VERSION