Die meisten langsamen Operationen in einem Programm sind Wartezeiten: darauf, dass ein Webserver antwortet, eine Datenbank Zeilen liefert, eine Datei gelesen wird. Mit async und await kann eine Methode während einer solchen Wartezeit pausieren, ohne einen Thread festzuhalten, und dann dort weitermachen, wo sie aufgehört hat. Der Code liest sich trotzdem von oben nach unten wie gewöhnlicher Code.
Eine erste async-Methode
Eine async-Methode gibt Task (kein Ergebnis) oder Task<T> (ein Ergebnis vom Typ T) zurück. Darin wartet await auf einen anderen Task und gibt dir sein Ergebnis.
Ausgabe:
Tea 2.50, cake 4.00, total 6.50
GetPriceAsync sagt return 2.50m, und der Compiler verpackt das in den Task<decimal>, den die Methode zurückgibt. await packt ihn wieder aus. Task.Delay ist die async-Version von Thread.Sleep: Es ist nach Ablauf der Zeit fertig, ohne in der Zwischenzeit einen Thread zu blockieren.
Seit C# 7.1 kann Main selbst async sein, und so würdest du dieses Programm in modernem .NET schreiben:
static async Task Main()
{
decimal tea = await GetPriceAsync("tea");
Console.WriteLine(tea);
}
Die ausführbaren Beispiele auf dieser Seite rufen eine async-Methode RunAsync aus einem normalen Main mit .GetAwaiter().GetResult() auf, was der Compiler für ein async Main erzeugt. In einer Konsolen-App ist das sicher; der Abschnitt zu Deadlocks erklärt, warum derselbe Aufruf in UI-Code gefährlich ist.
Was bei einem await passiert
Eine async-Methode läuft synchron bis zu ihrem ersten await auf einen unfertigen Task. An dieser Stelle gibt sie ihrem Aufrufer einen Task zurück, und der Rest der Methode wird zu einer Fortsetzung, die läuft, wenn der abgewartete Task fertig ist.
Ausgabe:
Calling DownloadAsync
Download: starting
DownloadAsync returned a task; doing other work
Download: finished
Download awaited
„Download: starting“ wird ausgegeben, bevor DownloadAsync zurückkehrt, weil alles bis zum ersten await auf dem Thread des Aufrufers läuft. Dann gibt die Methode einen unfertigen Task zurück, der Aufrufer macht weiter, und „Download: finished“ erscheint erst, wenn die Verzögerung vorbei ist. Eine async-Methode aufzurufen startet sie; sie abzuwarten ist der Weg, auf das Ergebnis zu warten.
async erzeugt keinen Thread. Während die Methode pausiert, wartet überhaupt kein Thread auf sie, deshalb kann ein Server tausende Anfragen auf eine Datenbank warten lassen und dabei nur eine Handvoll Threads verwenden. Für CPU-lastige Arbeit, die auf einem anderen Thread laufen soll, nimm Task.Run, behandelt unter Tasks.
Operationen gleichzeitig mit Task.WhenAll ausführen
Einen Aufruf nach dem anderen abzuwarten ist sequenziell: Jeder startet erst, nachdem der vorherige fertig ist. Wenn die Operationen nicht voneinander abhängen, starte sie alle und warte sie dann gemeinsam mit Task.WhenAll ab:
Beispielausgabe:
Sequential: 160 units in ~900 ms
Concurrent: 160 units in ~300 ms
Die sequenzielle Version braucht die Summe der drei Wartezeiten, die nebenläufige ungefähr so lange wie die langsamste. Task.WhenAll gibt die Ergebnisse in derselben Reihenfolge wie die übergebenen Tasks zurück, egal welcher zuerst fertig war. Es funktioniert auch mit einer Liste, der üblichen Form für „hol jedes Element“: await Task.WhenAll(ids.Select(id => FetchAsync(id))).
Task.WhenAny ist das Gegenstück, das fertig ist, sobald der erste Task fertig ist, nützlich für Timeouts und „die erste Antwort gewinnt“.
Exceptions in async-Code
Eine in einer async-Methode geworfene Exception wird im zurückgegebenen Task gespeichert und erneut geworfen, wenn der Task abgewartet wird, ein gewöhnliches try/catch um das await funktioniert also:
Ausgabe:
Task created, completed yet: False
Caught ArgumentOutOfRangeException for userId
WhenAll rethrew the first failure
All failures: 1
The other task still succeeded: profile 7
Der Aufruf von LoadProfileAsync(-1) hat nicht geworfen: Die Exception lebt im Task, bis etwas ihn abwartet. Ein Task, den nie jemand abwartet, verbirgt seine Exception vollständig, ein weiterer Grund, immer abzuwarten, was du startest. Bei WhenAll wirft await die erste Exception erneut, während die Property Exception des kombinierten Tasks (eine AggregateException) alle enthält. ok.Result zu lesen ist dort in Ordnung, weil ok bereits fertig ist.
async void: nur für Event-Handler
Eine async-Methode kann auch void zurückgeben. Vermeide das überall außer bei Event-Handlern:
// Bad: the caller cannot await it or catch its exceptions.
static async void SaveAsync(Order order)
{
await db.InsertAsync(order); // if this throws, the process may crash
}
// Good: return Task, so callers can await and handle errors.
static async Task SaveAsync(Order order)
{
await db.InsertAsync(order);
}
// Acceptable: an event handler must return void.
private async void SaveButton_Click(object sender, EventArgs e)
{
try { await SaveAsync(currentOrder); }
catch (Exception ex) { ShowError(ex); }
}
Bei async void gibt es keinen Task zum Abwarten, der Aufrufer macht also weiter, bevor die Arbeit erledigt ist, und eine darin geworfene Exception wird direkt im Synchronisationskontext (oder im Thread-Pool) ausgelöst, wo kein try/catch eines Aufrufers sie erreicht. In einer Konsolen- oder Server-App beendet das meist den Prozess. Fang in einem async void Event-Handler alles selbst ab.
Ein verwandter Fehler ist, eine async-Methode ohne await aufzurufen. Der Compiler warnt (CS4014), und die Methode läuft im Hintergrund, ohne dass jemand ihr Ergebnis oder ihre Fehler beobachtet.
Deadlocks durch .Result und .Wait()
Mit .Result oder .Wait() auf einen Task zu blockieren ist die Stelle, an der async-Code am häufigsten schiefgeht. In einer Anwendung mit Synchronisationskontext (WinForms, WPF, MAUI, klassisches ASP.NET) ist der Ablauf:
- Der UI-Thread ruft
GetDataAsync().Resultauf und blockiert, während er auf den Task wartet. - In
GetDataAsyncist einawaitfertig. Standardmäßig muss seine Fortsetzung in dem Kontext laufen, in dem sie begonnen hat: dem UI-Thread. - Der UI-Thread ist in Schritt 1 blockiert, die Fortsetzung läuft also nie, der Task wird nie fertig, und Schritt 1 endet nie.
Die App friert ohne Exception ein. Konsolen-Apps und ASP.NET Core haben keinen solchen Kontext, deshalb funktioniert derselbe Code in einem Testprogramm und hängt in einer Desktop-App. Die Lösungen:
- Nimm
awaitdie ganze Aufrufkette hinauf, statt zu blockieren („async all the way“). Event-Handler können dafürasync voidsein. - In Bibliothekscode, der nicht in den Kontext des Aufrufers zurückkehren muss, schreibe
await SomethingAsync().ConfigureAwait(false);. Die Fortsetzung läuft dann im Thread-Pool statt im erfassten Kontext, und die Bibliothek spart sich einen unnötigen Threadwechsel. Einen blockierenden Aufrufer schützt das nur, wenn jedesawaitin der Kette dasselbe tut, betrachte es also als gute Praxis für Bibliotheken, nicht als Lösung für.Result.
Anwendungscode in ASP.NET Core braucht kein ConfigureAwait(false), weil es keinen Kontext gibt, in den er zurückkehren müsste.
Häufige Fehler
- Unabhängige Aufrufe einzeln abwarten. Starte sie, dann
await Task.WhenAll(...). async void-Methoden. GibTaskzurück; behalteasync voidfür Event-Handler..Resultund.Wait()in UI- oder klassischem ASP.NET-Code. Sie verursachen Deadlocks; warte stattdessen mit await.awaitvergessen. Die Arbeit läuft unbeobachtet, und ihre Exceptions verschwinden.- I/O in
Task.Runverpacken. Eine async-API wieFile.ReadAllTextAsyncoderHttpClient.GetStringAsyncgibt den Thread bereits frei;Task.Runfügt nur einen Umweg über den Thread-Pool hinzu. - Annehmen, dass
async„läuft auf einem anderen Thread“ bedeutet. Es bedeutet „kann pausieren, ohne zu blockieren“. NimmTask.Runfür CPU-lastige Arbeit.
Häufig gestellte Fragen
Wie funktionieren async und await in C#?
Eine als async markierte Methode darf await verwenden. Erreicht die Methode ein await auf einen noch nicht fertigen Task, kehrt sie sofort zu ihrem Aufrufer zurück und gibt ihm einen Task, der den Rest der Arbeit darstellt. Wenn die abgewartete Operation fertig ist, macht die Methode nach dem await weiter. Während des Wartens ist kein Thread blockiert.
Was ist der Unterschied zwischen Task und Task<T>?
Task steht für eine Operation, die keinen Wert erzeugt, die async-Version einer void-Methode. Task<T> steht für eine, die ein T erzeugt: await auf einen Task<int> gibt dir das int. Eine als async Task<int> deklarierte Methode schreibt einfach return 42;, und der Compiler verpackt das in den Task.
Wie führe ich mehrere async-Operationen gleichzeitig aus?
Starte sie zuerst alle und warte sie dann gemeinsam ab: var a = GetUserAsync(); var b = GetOrdersAsync(); await Task.WhenAll(a, b);. await GetUserAsync(); await GetOrdersAsync(); zu schreiben führt sie nacheinander aus, die Gesamtzeit ist also die Summe statt der längsten.
Warum ist async void in C# schlecht?
Eine async void-Methode lässt sich nicht abwarten, der Aufrufer weiß also nicht, wann sie fertig ist, und eine darin geworfene Exception kann der Aufrufer nicht abfangen: Sie wird im Synchronisationskontext ausgelöst und lässt den Prozess meist abstürzen. Gib stattdessen Task zurück. async void existiert nur für Event-Handler, deren Signatur void verlangt.
Warum verursacht .Result oder .Wait() einen Deadlock?
In einer App mit Synchronisationskontext (WinForms, WPF, klassisches ASP.NET) blockiert .Result den UI- oder Request-Thread, während die Fortsetzung der abgewarteten Methode darauf wartet, genau auf diesem Thread zu laufen. Jeder wartet ewig auf den anderen. Nimm durchgehend await statt zu blockieren und in Bibliothekscode ConfigureAwait(false).
Kann Main in C# async sein?
Ja, seit C# 7.1: static async Task Main() oder static async Task<int> Main(). Top-Level-Anweisungen (C# 9) können await direkt verwenden. In älterem Code ist die Entsprechung, MainAsync().GetAwaiter().GetResult() aus einem normalen Main aufzurufen, was dort sicher ist, weil eine Konsolen-App keinen Synchronisationskontext hat.