C# 5: Es wird asynchron

8. Dezember 2010

Am Anfang war C# 1 und brachte uns managed Code. Danach kamen die Generics mit C# 2, deklarative Programmierung in C# 3 und dynamische Programmierung in C# 4.

C# 5 wird uns wohl die asynchrone Programmierung vereinfachen. Dies zumindest hat Anders Hejlsberg, seines Zeichens Chefentwickler von C#, auf einer Session der PDC10 angekündigt und dabei auch bereits einige Beispiele vorgezeigt.

Bisher ist asynchrone Programmierung vielen Entwicklern immer ein Dorn im Auge und das zurecht. Methoden werden asynchron aufgerufen, kehren dann irgendwann zurück und ihre Ergebnisse müssen z.B. in einer Callback-Methode separat abgehandelt werden. Mit Lambdas gewinnt man hier ein wenig an Übersicht, das grundlegende Problem bleibt allerdings bestehen: Code lässt sich nicht mehr sequentiell in logischer Abfolge schreiben und wird damit unleserlich(er) und schlechter wartbar. Auch Dinge wie Fehlerbehandlung wollen hier richtig implementiert sein.

Für C# 5 sind zwei neue Schlüsselwörter async und await angekündigt, welche dieses Problem angehen sollen (wobei sich die Syntax aufgrund des frühen Stadiums der Entwicklung noch ändern kann). Sie bauen auf Features von .NET 4.0 auf, die in der Task Parallel Library (TPL) vorhanden sind, brechen diese nun allerdings auf Sprachenebene herunter, was Asynchronität sehr elegant macht.

Mit async werden dabei Methoden deklariert, die irgendeinen asynchronen Kontrollfluss beinhalten, selber aber nicht unbedingt asynchron aufgerufen werden müssen. async Methoden beinhalten ein oder mehrere await Statements, welche die eigentliche asynchrone Verarbeitung abstrahieren. Mit async weiß der Compiler, dass er die entsprechende Methode in einen Zustandsautomaten übersetzen muss, der im Prinzip das Gleiche macht wie der Programmierer bisher manuell: Callbacks setzen und bei Rückkehr der asynchronen Funktionalität aufzurufen… async Methoden müssen als Rückgabewert void, Task oder Task<T> haben, doch dazu später noch mehr…

await gibt einen Punkt in einer async Methode an, an dem tatsächlich eine asynchrone Funktionalität aufgerufen wird. await sagt dabei: "rufe diese Methode asynchron auf und setze bei ihrer Rückkehr an dieser Stelle fort". Die Implikation davon: man kann Code wie bisher sequentiell schreiben, um die Asynchronität und deren Abarbeitung kümmert sich der Compiler!

Es sei mal an einem Beispiel illustriert, das im Async Whitepaper enthalten ist.

Nehmen wir folgende synchrone Methode:

   1: public int SumPageSizes(IList<Uri> uris) {

   2:     int total = 0;

   3:     foreach (var uri in uris) {

   4:         statusText.Text = string.Format("Found {0} bytes ...", total);

   5:         var data = new WebClient().DownloadData(uri);

   6:         total += data.Length;

   7:     }

   8:     statusText.Text = string.Format("Found {0} bytes total", total);

   9:     return total;

  10: }

Diese kann mit den neuen Schlüsselwörtern wie folgt in eine asynchrone Methode umgewandelt werden:

   1: public async Task<int> SumPageSizesAsync(IList<Uri> uris) {

   2:     int total = 0;

   3:     foreach (var uri in uris) {

   4:         statusText.Text = string.Format("Found {0} bytes ...", total);

   5:         var data = await new WebClient().DownloadDataAsync(uri);

   6:         total += data.Length;

   7:     }

   8:     statusText.Text = string.Format("Found {0} bytes total", total);

   9:     return total;

  10: }

Und das war’s! Die gelben Markierungen geben alle Änderungen im Vergleich zum Code darüber an, die notwendig waren, um den Code asynchron zu machen. Die Sequentialität und die Übersichtlichkeit des Codes bleiben dabei voll gewahrt! await kommt beim Aufruf der asynchronen Methode DownloadDataAsync() zum Einsatz. Es wartet an dieser Stelle nicht (!) auf die Rückkehr der Methode, denn dann wären wir wieder in der synchronen Welt. Im Gegensatz dazu ist es nur eine Anweisung für den Compiler bei Rückkehr der Methode an dieser Stelle weiterzumachen. Dabei können die Rückgabewerte der Methode direkt an eine Variable zugewiesen werden, Auswertungen á la e.Result entfallen!

So elegant ich das finde, zwei Dinge habe ich festgestellt, die mich erstmal zum Nachdenken gebracht haben:

  1. async Methoden müssen beim Aufrufer IMMER mit einem await abgehandelt werden. Das impliziert Folgendes: wenn ich eine Aufrufkette von meiner UI über 5 Methoden an meinen Datenzugriff habe und dort eine async Methode definiere, dann müssen ALLE Methoden der Aufrufkette async sein und mit await die Ergebnisse der Methoden darunter abarbeiten. Das fühlt sich für mich erstmal komisch an, andererseits ist es auch genau das, was man machen müsste, wenn man manuell Asynchronität behandelt und dabei Ergebnisse über die Aufrufkette hinweg zum UI bringen will. Hat man sich daran gewöhnt, sollte es kein Problem sein…
  2. Nehmen wir obiges Beispiel: der Rückgabewert der async Methode ist zwar Task<int>, de facto wird vom Code mit "int total" aber ein Integer-Wert zurückgegeben. Wie das? Die async Methode wird vom Compiler umgeschrieben und gibt daher einen Task<T> zurück, auch wenn im Code etwas anderes steht. Aber es gibt hier einen Mismatch zwischen Deklaration und tatsächlicher Definition, was ich auch als äußerst gewöhnungsbedürftig sehe. Hier sehe ich Entwickler, die mit diesem Konzept überfordert sein könnten…

Alles in allem bleibt meine Aussage trotzdem bestehen: ich halte das für eine echt coole Sache! Gerade in der heutigen immer asynchroner werdenden Welt zwischen Cloud, Silverlight und Windows Phone 7 ist solch ein "syntactic sugar" mehr als überfällig…

Btw: Die neuen Schlüsselwörter können direkt ausprobiert werden. Einfach die Async CTP herunterladen und loslegen: Async CTP