Zum Inhalt
wannacode

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 einen decimal liefert, zum Beispiel einen Preis. Ein Task ohne Typ sagt nur „fertig“.
  • Eine Methode, die auf etwas wartet, wird mit async markiert und gibt einen Task zurück. Nach Konvention endet ihr Name auf Async.
  • await vor 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 + " €");
Ausgabe
Ich frage nach dem Preis...
Preis: 24.90 €
  • async Task<decimal>: Die Methode ist asynchron und liefert am Ende einen decimal. Im Rumpf schreibst du return 24.90m, nicht das return eines Tasks: Den Task baut der Compiler.
  • await darfst du nur in einer async-Methode oder im Code auf oberster Ebene des Programms schreiben.
  • await Warehouse.GetPriceAsync(…) gibt dir direkt den decimal. Der Task wird „ausgepackt“.
Genauer, für Neugierige
  • Mit der Hülle class Program wird auch die Methode Main asynchron: static async Task Main(). Code auf oberster Ebene mit await ist eine Kurzform genau dafür.
class Program
{
static async Task Main()
{
decimal price = await Warehouse.GetPriceAsync("Pfefferminze");
Console.WriteLine("Preis: " + price + " €");
}
}
Ausgabe
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.Delay vertritt 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;
}
Antwort

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 + " €");
}
Ausgabe
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] + " €");
}
Ausgabe
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
  1. Drei Produkte. Das Lager beantwortet jede Anfrage nach 300 ms.
  2. await hält die Schleife an, bis das Lager für „Espressobohnen“ antwortet. Die nächste Anfrage hat noch nicht begonnen.
  3. await hält die Schleife an, bis das Lager für „Sencha-Grüntee“ antwortet. Die nächste Anfrage hat noch nicht begonnen.
  4. await hält die Schleife an, bis das Lager für „Pfefferminze“ antwortet. Die nächste Anfrage hat noch nicht begonnen.
  5. Nacheinander: 3 × 300 = 900 ms.
  6. Select startet alle drei Anfragen auf einmal. Jede gibt sofort einen Task zurück, ohne zu warten.
  7. await Task.WhenAll wartet auf alle Tasks. Die drei Wartezeiten laufen gleichzeitig.
  8. 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 WhenAll nur, und die Ergebnisse holst du dir mit await aus jedem Task. Der Task ist schon fertig, also wartet dieses await nicht:
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;
}
Ausgabe
Pfefferminze: 4.20 €, 14 auf Lager
  • Falle: .Result und .Wait(). task.Result liefert das Ergebnis und task.Wait() wartet, aber beide blockieren das Programm, bis der Task fertig ist. Der Vorteil von async ist 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 einer AggregateException. Schreib await.
  • Falle: async void. Eine async void-Methode gibt keinen Task zurück, also kann niemand auf sie warten, und ihre Exception lässt sich nicht mit einem try um den Aufruf fangen. Schreib async Task. async void ist 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);

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");
}
Ausgabe
Dieses Produkt gibt es nicht
Genauer, für Neugierige
  • Wenn in Task.WhenAll mehrere Tasks scheitern, wirft await die erste Exception. Die anderen stehen in task.Exception des jeweiligen Tasks. Die Tasks, die nicht gescheitert sind, haben ein Ergebnis, also kannst du einzeln auf sie warten, jeden in seinem eigenen try (Aufgabe 2 der Übung).
  • Wenn eine Anfrage zu lange dauern könnte, bricht echter Code sie nach einer Weile mit einem CancellationToken ab. Du begegnest ihm in HttpClient und 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.

Ü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:

Ausgabe
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:

Ausgabe
Pfefferminze: 4.20 €
Kakao: nicht im Lager
Espressobohnen: 24.90 €
Zu zahlen: 29.10 €

Test

Teste dich

1. Was gibt eine Methode async Task<decimal> GetPriceAsync() zurück, wenn du sie ohne await aufrufst?
2. Drei Anfragen von je 300 ms werden mit Task.WhenAll gestartet. Wie lange dauert das Warten ungefähr?
3. Eine async-Methode wirft nach await Task.Delay(300) eine Exception. Wo fängst du sie?
4. Warum ist task.Result eine Falle?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·