async und await
Kurz gesagt: async und await in C# warten auf lange Arbeit, zum Beispiel eine Anfrage, ohne das Programm anzuhalten: Ein Task ist Arbeit, die später fertig wird, und Task.WhenAll wartet auf mehrere gleichzeitig.
Problem
Drei Anfragen von je 300 ms
Eine Produktseite im Onlineshop fragt das Lager nach dem Preis. Das Lager ist ein anderes Programm und antwortet nach 300 ms. Wenn das Programm untätig wartet, tut es in diesen 300 ms nichts anderes. Ein Webserver mit 1000 Nutzern, der so auf jeden einzelnen wartet, kommt zum Stillstand: Alle stehen in der Schlange hinter einem langsamen Lager.
Ein Warenkorb mit drei Produkten ist noch schlimmer. Die drei Anfragen nacheinander dauern 900 ms, obwohl das Lager auf alle drei zugleich in 300 ms antworten könnte.
Die Idee
Ein Task statt Warten
Wenn du im Café an der Theke bestellst, bleibst du nicht an der Theke stehen, bis dein Kaffee fertig ist. Du bekommst eine Nummer und machst etwas anderes, und wenn der Kaffee fertig ist, holst du ihn ab.
In C# ist die Nummer ein Task: ein Objekt für Arbeit, die später fertig wird. Task.Run und .Result kennst du aus der Lektion „Threads“: Dort wartet .Result, indem es den Thread beschäftigt hält. Hier warten wir, ohne ihn festzuhalten.
Task<decimal>ist ein Task, der am Ende einendecimalliefert, zum Beispiel einen Preis. EinTaskohne Typ sagt nur „fertig“.- Eine Methode, die auf etwas wartet, wird mit
asyncmarkiert und gibt einen Task zurück. Nach Konvention endet ihr Name aufAsync. awaitvor einem Task bedeutet „warte auf das Ergebnis, aber ohne das Programm anzuhalten“. Solange der Task nicht fertig ist, kann das Programm andere Arbeit erledigen, zum Beispiel den nächsten Kunden bedienen.
Sieh es dir an
async und await
Das Lager ist eine Klasse mit einer Methode. Task.Delay(300) ist ein Task, der nach 300 ms fertig ist. Hier spielt er das langsame Lager:
class Warehouse{ static Dictionary<string, decimal> prices = new Dictionary<string, decimal> { { "Espressobohnen", 24.90m }, { "Sencha-Grüntee", 9.80m }, { "Pfefferminze", 4.20m }, };
public static async Task<decimal> GetPriceAsync(string product) { await Task.Delay(300); return prices[product]; }}Das Programm fragt nach dem Preis und wartet mit await darauf:
Console.WriteLine("Ich frage nach dem Preis...");decimal price = await Warehouse.GetPriceAsync("Espressobohnen");Console.WriteLine("Preis: " + price + " €");Ich frage nach dem Preis...Preis: 24.90 €async Task<decimal>: Die Methode ist asynchron und liefert am Ende einendecimal. Im Rumpf schreibst dureturn 24.90m, nicht dasreturneines Tasks: Den Task baut der Compiler.awaitdarfst du nur in einerasync-Methode oder im Code auf oberster Ebene des Programms schreiben.await Warehouse.GetPriceAsync(…)gibt dir direkt dendecimal. Der Task wird „ausgepackt“.
Genauer, für Neugierige
- Mit der Hülle
class Programwird auch die MethodeMainasynchron:static async Task Main(). Code auf oberster Ebene mitawaitist eine Kurzform genau dafür.
class Program{ static async Task Main() { decimal price = await Warehouse.GetPriceAsync("Pfefferminze"); Console.WriteLine("Preis: " + price + " €"); }}Preis: 4.20 €- Die echte Arbeit, auf die du wartest, ist das Netzwerk, die Festplatte oder eine Datenbank:
HttpClient(die Lektion „Ein Webserver mit Minimal API“),File.ReadAllTextAsync, Datenbankabfragen.Task.Delayvertritt sie hier nur, damit der Code ohne Netzwerk läuft.
Probier es aus
In welcher Reihenfolge
Was gibt das Programm aus?
Console.WriteLine("Bestellung angenommen");Task<decimal> priceTask = GetPriceAsync();Console.WriteLine("Ich warte auf den Preis");decimal price = await priceTask;Console.WriteLine("Preis: " + price + " €");
static async Task<decimal> GetPriceAsync(){ Console.WriteLine("Ich frage das Lager"); await Task.Delay(300); Console.WriteLine("Das Lager hat geantwortet"); return 24.90m;}Die async-Methode startet sofort und läuft bis zum ersten await auf etwas Unfertiges: Sie gibt „Ich frage das Lager“ aus. Dort gibt sie den Task zurück, also kommt als Nächstes „Ich warte auf den Preis“. await priceTask wartet 300 ms, dann macht die Methode mit „Das Lager hat geantwortet“ weiter und gibt den Preis zurück.
Sieh es dir an
Nacheinander oder gleichzeitig
Der Warenkorb hat drei Produkte:
List<string> products = new List<string> { "Espressobohnen", "Sencha-Grüntee", "Pfefferminze" };Mit await in der Schleife wartet jede Anfrage auf die vorige:
// Nacheinander: etwa 900 ms.foreach (string product in products){ decimal price = await Warehouse.GetPriceAsync(product); Console.WriteLine(product + ": " + price + " €");}Espressobohnen: 24.90 €Sencha-Grüntee: 9.80 €Pfefferminze: 4.20 €Wenn du zuerst alle drei Tasks startest und erst dann wartest, laufen sie gleichzeitig. Task.WhenAll ist ein Task, der fertig ist, wenn alle fertig sind, und er liefert ihre Ergebnisse in einem Array, in der Reihenfolge der Tasks:
// Gleichzeitig: etwa 300 ms.List<Task<decimal>> tasks = products.Select(p => Warehouse.GetPriceAsync(p)).ToList();decimal[] prices = await Task.WhenAll(tasks);for (int i = 0; i < products.Count; i++){ Console.WriteLine(products[i] + ": " + prices[i] + " €");}Espressobohnen: 24.90 €Sencha-Grüntee: 9.80 €Pfefferminze: 4.20 €Select aus der LINQ-Lektion ruft GetPriceAsync für jedes Produkt auf. Die Methode wartet nicht, sondern gibt einen Task zurück, also ist tasks eine Liste mit drei Tasks, die schon laufen. Die Ausgabe ist dieselbe, aber das Warten dauert so lange wie die langsamste Anfrage, etwa 300 ms statt 900.
foreach (string product in products){ decimal price = await Warehouse.GetPriceAsync(product); Console.WriteLine(product + ": " + price + " €");}
List<Task<decimal>> tasks = products.Select(p => Warehouse.GetPriceAsync(p)).ToList();decimal[] prices = await Task.WhenAll(tasks);for (int i = 0; i < products.Count; i++){ Console.WriteLine(products[i] + ": " + prices[i] + " €");}Schritt für Schritt, in Worten
- Drei Produkte. Das Lager beantwortet jede Anfrage nach 300 ms.
- await hält die Schleife an, bis das Lager für „Espressobohnen“ antwortet. Die nächste Anfrage hat noch nicht begonnen.
- await hält die Schleife an, bis das Lager für „Sencha-Grüntee“ antwortet. Die nächste Anfrage hat noch nicht begonnen.
- await hält die Schleife an, bis das Lager für „Pfefferminze“ antwortet. Die nächste Anfrage hat noch nicht begonnen.
- Nacheinander: 3 × 300 = 900 ms.
- Select startet alle drei Anfragen auf einmal. Jede gibt sofort einen Task zurück, ohne zu warten.
- await Task.WhenAll wartet auf alle Tasks. Die drei Wartezeiten laufen gleichzeitig.
- Nach 300 ms sind alle drei Preise da: 300 ms statt 900. Die Ausgabe ist dieselbe.
Genauer, für Neugierige
- Die Tasks können verschiedene Typen haben. Dann wartet
WhenAllnur, und die Ergebnisse holst du dir mitawaitaus jedem Task. Der Task ist schon fertig, also wartet diesesawaitnicht:
Task<decimal> priceTask = Warehouse.GetPriceAsync("Pfefferminze");Task<int> stockTask = GetStockAsync("Pfefferminze");await Task.WhenAll(priceTask, stockTask);Console.WriteLine("Pfefferminze: " + await priceTask + " €, " + await stockTask + " auf Lager");
static async Task<int> GetStockAsync(string product){ await Task.Delay(300); return 14;}Pfefferminze: 4.20 €, 14 auf Lager- Falle:
.Resultund.Wait().task.Resultliefert das Ergebnis undtask.Wait()wartet, aber beide blockieren das Programm, bis der Task fertig ist. Der Vorteil vonasyncist weg, und in einer App mit Fenstern (WPF) oder in einem alten ASP.NET-Projekt kann das Programm für immer hängen bleiben: Es wartet auf einen Task, der auf das Programm wartet. Dazu kommt die Exception verpackt in einerAggregateException. Schreibawait. - Falle:
async void. Eineasync void-Methode gibt keinen Task zurück, also kann niemand auf sie warten, und ihre Exception lässt sich nicht mit einemtryum den Aufruf fangen. Schreibasync Task.async voidist nur für Event-Handler da (die WPF-Lektion).
Probier es aus
Auf alle warten
Welche Methode wartet auf alle Tasks in der Liste und liefert ihre Ergebnisse?
decimal[] prices = await Task.(tasks);Task.WhenAll(tasks) ist fertig, wenn alle Tasks fertig sind. Mit await liefert es ein Array mit den Ergebnissen in der Reihenfolge der Tasks.
Sieh es dir an
Wenn das Lager Nein sagt
Das Lager hat keinen Kakao. prices[product] wirft eine KeyNotFoundException, wie in der Lektion über Dictionaries. Eine Exception in einer async-Methode wird im Task aufbewahrt und kommt bei await heraus. Also fängst du sie wie gewohnt, mit einem try um das await:
try{ decimal price = await Warehouse.GetPriceAsync("Kakao"); Console.WriteLine("Preis: " + price + " €");}catch (KeyNotFoundException){ Console.WriteLine("Dieses Produkt gibt es nicht");}Dieses Produkt gibt es nichtGenauer, für Neugierige
- Wenn in
Task.WhenAllmehrere Tasks scheitern, wirftawaitdie erste Exception. Die anderen stehen intask.Exceptiondes jeweiligen Tasks. Die Tasks, die nicht gescheitert sind, haben ein Ergebnis, also kannst du einzeln auf sie warten, jeden in seinem eigenentry(Aufgabe 2 der Übung). - Wenn eine Anfrage zu lange dauern könnte, bricht echter Code sie nach einer Weile mit einem
CancellationTokenab. Du begegnest ihm inHttpClientund im Webserver.
Probier es aus
Der Fehler, der nicht gefangen wurde
Das Programm soll „Dieses Produkt gibt es nicht“ ausgeben, bricht aber mit einer KeyNotFoundException ab. In welcher Zeile liegt das Problem?
Wähle die Zeile mit dem Fehler.
Der Aufruf von GetPriceAsync wirft nichts: Er gibt nur den Task zurück, und die Exception wird darin aufbewahrt. Sie kommt in Zeile 11 heraus, bei await task, und dort gibt es kein try mehr. Die Lösung ist, das await in das try zu holen: decimal price = await Warehouse.GetPriceAsync("Kakao");.
Übung
Die Preise der Bestellung
Löse die Aufgaben in deinem Editor. Verwende die Klasse Warehouse aus der Lektion.
1. Nacheinander und gleichzeitig. Hol die Preise der drei Produkte einmal nacheinander und einmal mit Task.WhenAll, dann gib die Summe aus. Schreib unter die beiden Wege die Kommentare // etwa 900 ms und // etwa 300 ms. Wenn du es messen willst: System.Diagnostics.Stopwatch.StartNew() startet eine Stoppuhr, und ElapsedMilliseconds sagt dir, wie viel Zeit vergangen ist:
Nacheinander: Espressobohnen: 24.90 € Sencha-Grüntee: 9.80 € Pfefferminze: 4.20 €Gleichzeitig: Espressobohnen: 24.90 € Sencha-Grüntee: 9.80 € Pfefferminze: 4.20 €Summe: 38.90 €2. Eine abgelehnte Anfrage. Die Bestellung ist Pfefferminze, Kakao und Espressobohnen. Starte alle drei Anfragen gleichzeitig und warte dann auf jede in ihrem eigenen try. Für das fehlende Produkt gib „nicht im Lager“ aus, und bilde die Summe nur aus den Produkten, die da sind:
Pfefferminze: 4.20 €Kakao: nicht im LagerEspressobohnen: 24.90 €Zu zahlen: 29.10 €Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: static: was alle Objekte teilen · Exceptions · Threads · Listen, Dictionaries und LINQ
- Geht weiter in: Ein Webserver mit Minimal API · MVC: Model, View, Controller · Views mit Razor · WebAPI: Daten für andere Programme
- Aufgaben zum Thema: Web und Desktop