Mit Abstand mein Lieblingsfeature in .NET 4.5 ist async
/await
oder, wie Microsoft es nennt, das Task-based Asynchronous Pattern (TAP). Ich wusste nicht wirklich, dass ich das all die Jahre hätte brauchen können, bis ich eine Aufzeichnung von Anders Hejlsbergs Build-Vortrag zu dem Thema sah. Kurz danach fand ich mich in einem stark asynchronen C++-Embedded-Projekt vor, das über ein Jahr dauerte und in dem ich mich nicht wohl fühlte, eine State Machine nach der anderen zu bauen um das inherente Problem aller asynchroner Anwendungen zu lösen: Was tun, wenn eine asynchrone Operation abgeschlossen ist?
Diese Blogserie wendet sich an C#-Entwickler, die sich für async
/await
interessieren. Ein Verständnis der mit .NET 4.0 eingeführten Task
-Klasse ist von Vorteil. Im ersten Teil erkläre ich das async
aus async
/await
.
Was ist async?
Mit dem async
-Schlüsselwort kann eine Method oder ein Lambda dekoriert werden.
Hier sollte erwähnt werden, dass async
nicht Teil der Signatur der Methode ist, daher kann man beim Implementieren eines Interfaces oder beim Überschreiben einer virtuellen oder abstrakten Methode entscheiden, ob man async
verwendet oder nicht.
Rückgabewerte
Eine async
-Method darf nur void
, Task
oder Task<T>
für einen konkreten Typen T zurückgeben.
void
sollte als Rückgabewert soweit wie möglich vermieden werden und wird fast ausschließlich in Eventhandlern gebraucht, wodurch die Methode eine Fire-and-Forget-Methode wird, bei der der Aufrufer keine Möglichkeit hat, zu erkennen, wann die Methode fertig oder fehlgeschlagen ist.
Bei einer Methode, die Task
oder Task<T>
zurückgibt, sollte man der Konvention nach das Suffix Async
verwenden, um hervorzuheben, dass die Methode awaitable ist (unabhängig davon, ob die Implementierung async
verwendet oder nicht.
Task
als Rückgabewert sollte verwendet werden für Methoden, die, wenn sie synchron wären, void
zurückgeben würden. Task<T>
sollte verwendet werden für Methoden, die sonst einen Typen T
zurückgeben würden (d.h. alles außer void
). Den Task
kann man sich als das Objekt vorstellen, das der Aufrufer verwenden kann, um mitzubekommen, was denn aus der asynchronen Methode geworden ist, die er angestoßen hat.
Welche Auswirkung hat async
?
Durch das Schreiben von async
passieren zwei Dinge mit der Methode oder dem Lambdaausdruck:
- Es erlaubt die Verwendung von
await
innerhalb der Methode (siehe meinen nächsten Blogbeitrag in dieser Serie). - Wenn der Rückgabewert nicht
void
ist, übersetzt der Kompiler auf magische Weise diereturn
-Anweisung (oder die fehlendereturn
-Anweisung am Ende der Methode) in einenTask<T>
oderTask
.
Für eine Methode, die keine await
-Aufrufe beinhaltet, bedeutet das, dass eine abgeschlossener Task
zurückgegeben wird, ohne dass dies explizit angegeben werden muss. Für Das Beispiel oben heißt das, dass es sich genauso verhält, wie diese nicht-async
-Version:
Eine Methode, die ein await
durchläuft, gibt ein Task
-Objekt zurück, dessen Zustand auf IsCompleted
wechselt, sobald der letzte Aufruf, auf den await
aufgerufen wurde, abgeschlossen ist und der darauf folgende synchrone Code (falls vorhanden) anschließend ebenfalls abgeschlossen ist. (Mehr hierzu in meinem nächsten Blogbeitrag in dieser Serie zum await
-Schlüsselwort.)
Brauche ich async?
Methoden, die nur ein await
als allerletzte Anweisung beinhalten, können grundsätzlich auch ohne das async
-Schlüsselwort implementiert werden. Die Methode
ist z.B. äquivalent zu
Obwohl diese Methoden das gleiche Ergebnis liefern, wirkt die async
-Version besser lesbarer, auch wenn sie leicht langsamer ist. Der andere Unterschied an dieser Stelle ist, dass, sollte die Methode stream.FlushAsync()
eine Exception werfen, die Methode FlushTheStreamAsync()
nicht im Call Stack der Exception auftaucht (mehr hierzu im nächsten Blogbeitrag).
Wie hilft mir das weiter?
Wie bereits erwähnt, kann das zurückgegebene Task
-Objekt verwendet werden, um den Zustand des asynchronen Aufrufs zu analysieren (Läuft er noch? Ist er fertig? Ist er fehlgeschlagen? Wurde er abgebrochen?). Auch wenn man diese Untersuchungen über die diversen Methoden und Eigenschaften der Task
-Klasse möglich ist, ist es meistens deutlich einfacher, hierzu das await
-Schlüsselwort zu verwenden, das im nächsten Blogbeitrag erläutert wird.