async und await in C# 5

Keine Zeit? Hier die Kurzversion in 30 Sekunden.
- Mit Visual Studio 2012 und .NET 4.5 (erschienen am 15. August 2012) gibt es in C# 5 die Schlüsselwörter
asyncundawait.- Asynchroner Code liest sich damit wie normaler, sequenzieller Code. Callbacks und
BackgroundWorkerwerden in vielen Fällen überflüssig.- Fallstricke:
async voidnur für Event-Handler, kein.Resultoder.Wait()auf der UI, in BibliothekenConfigureAwait(false).- Java hat dafür kein Gegenstück, ein
Futureblockiert beim Abholen des Ergebnisses.
Seit dem 15. August liegen Visual Studio 2012 und .NET 4.5 vor, und das wichtigste Stück daran ist für mich C# 5 mit async und await. Ich habe in den letzten Wochen einige kleine Programme darauf umgestellt und bin ehrlich begeistert, auch wenn die Sache ein paar Haken hat. Hier der Weg von den alten Varianten zur neuen und die Stellen, an denen man sich verletzen kann.
Das Problem vorher
Wer in einer Desktopanwendung eine Webseite lädt, darf das nicht im UI-Thread tun, sonst friert das Fenster ein. Die klassische Lösung war ein Ereignis mit Callback:
var client = new WebClient();
client.DownloadStringCompleted += (s, e) =>
{
ergebnisLabel.Text = "Länge: " + e.Result.Length;
};
client.DownloadStringAsync(new Uri("http://www.example.com/"));Das funktioniert, aber sobald zwei Schritte nacheinander kommen, wandert der Code in verschachtelte Handler. Fehlerbehandlung wird dabei zäh, und die Reihenfolge, in der man den Code liest, ist nicht mehr die Reihenfolge, in der er läuft. Mit .NET 4.0 kam Task dazu, und damit ging es auch so:
Task.Factory.StartNew(() => new WebClient().DownloadString("http://www.example.com/"))
.ContinueWith(t =>
{
ergebnisLabel.Text = "Länge: " + t.Result.Length;
}, TaskScheduler.FromCurrentSynchronizationContext());Das ist besser, aber der Wechsel zurück in den UI-Thread muss ausdrücklich angefordert werden, und der erste Schritt blockiert einen Thread aus dem Pool, nur um zu warten. Der BackgroundWorker liegt in derselben Gegend: brauchbar, aber viel Zeremonie für wenig Inhalt.
Mit async und await
In C# 5 sieht dasselbe so aus:
private async void LadenButton_Click(object sender, EventArgs e)
{
ladenButton.Enabled = false;
try
{
using (var client = new HttpClient())
{
string html = await client.GetStringAsync("http://www.example.com/");
ergebnisLabel.Text = "Länge: " + html.Length;
}
}
catch (HttpRequestException ex)
{
ergebnisLabel.Text = "Fehler: " + ex.Message;
}
finally
{
ladenButton.Enabled = true;
}
}HttpClient liegt in System.Net.Http und ist ebenfalls neu in 4.5. Bei await passiert Folgendes: Die Methode läuft bis dorthin, gibt dann die Kontrolle an den Aufrufer zurück (der UI-Thread kann also weiter Nachrichten verarbeiten), und sobald der Download fertig ist, läuft sie hinter dem await weiter. Der Compiler baut dafür im Hintergrund eine Zustandsmaschine. Bei einer Windows-Forms- oder WPF-Anwendung landet die Fortsetzung wieder im UI-Thread, weil await den aktuellen SynchronizationContext merkt. Deshalb darf ich direkt danach das Label setzen. Ausnahmen werden ganz normal mit try/catch gefangen, und das finally stellt den Button wieder her. Das ist für mich der eigentliche Gewinn: Der Code steht in der Reihenfolge da, in der er abläuft.
Eigene asynchrone Methoden liefern Task oder Task<T> zurück und tragen per Konvention das Suffix Async:
public static async Task<int> ZaehleZeichenAsync(string url)
{
using (var client = new HttpClient())
{
string text = await client.GetStringAsync(url).ConfigureAwait(false);
return text.Length;
}
}Für rechenintensive Arbeit, die keine asynchrone Schnittstelle hat, gibt es Task.Run. Man sollte aber nicht den Fehler machen, async mit „läuft in einem anderen Thread“ gleichzusetzen. Ein await auf eine Netzwerkoperation belegt währenddessen gar keinen Thread.
Mehrere Aufgaben gleichzeitig
Reizvoll finde ich, dass sich auch parallele Abläufe einfach schreiben lassen. Man startet mehrere Tasks, ohne sofort zu warten, und wartet dann gemeinsam:
Task<string> a = client.GetStringAsync("http://www.example.com/a");
Task<string> b = client.GetStringAsync("http://www.example.com/b");
string[] seiten = await Task.WhenAll(a, b);Beide Downloads laufen gleichzeitig, und die Methode macht erst weiter, wenn beide fertig sind. Mit den alten Callbacks wäre dafür ein Zähler samt Sperre nötig gewesen, den man leicht falsch baut. Wirft einer der Tasks eine Ausnahme, bekommt man sie beim await zu sehen. Das sollte man beim Testen einmal bewusst ausprobieren, damit man weiß, was im Fehlerfall passiert.
Die Fallstricke
async void ist nur für Event-Handler gedacht, wie oben beim Button. Alles andere sollte Task zurückgeben, weil der Aufrufer sonst weder auf das Ende warten noch Ausnahmen abfangen kann. Eine Ausnahme in einer async void-Methode landet im Kontext und kann das Programm abstürzen lassen.
.Result und .Wait() blockieren den aufrufenden Thread. Auf dem UI-Thread führt das schnell zum Deadlock: Der UI-Thread wartet auf den Task, der Task will nach dem await zurück in den UI-Thread, und der ist belegt. Dasselbe passiert in ASP.NET, wo der Anfragekontext sich ähnlich verhält. Wer das so einmal erlebt hat, vergisst es nicht. Wer in einer asynchronen Kette steckt, bleibt dort und verwendet durchgehend await.
ConfigureAwait(false) sagt await, dass die Fortsetzung nicht in den ursprünglichen Kontext zurückmuss. In Bibliotheken, die keine UI anfassen, würde ich das grundsätzlich setzen. Es spart den Wechsel und nimmt außerdem der Deadlock-Falle die Spitze, falls ein Aufrufer doch einmal .Result benutzt. In UI-Code dagegen lasse ich es weg, weil ich dort ja gerade zurück in den UI-Thread will.
Vergleich mit Java
Wer auch mit Java arbeitet, sucht vergeblich nach etwas Vergleichbarem. Java 7 hat Future, ExecutorService und das neue Fork/Join-Framework. Aber ein Future.get() blockiert, bis das Ergebnis da ist, und die Fortsetzung muss man selbst in einen Callback packen. Es gibt keine Sprachunterstützung, die einem den Code in die lineare Form umbaut. Hier ist C# mit dem Compiler im Vorteil, und das ist keine Kleinigkeit, denn die Fehlerbehandlung bleibt dabei ganz normales try/catch.
Für alles, was auf Netzwerk, Dateien oder Datenbank wartet, nehme ich async und await künftig als Standard. Die Regeln oben muss man lernen, aber der Rest wird dafür deutlich lesbarer.
Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (Visual-Studio-2012-Logo: Microsoft, gemeinfrei); die Marken gehören ihren Inhabern.
Christian Ackermann
Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.