In Teil 1 dieser Serie haben wir uns mit dem async
-Schlüsselwort auseinandergesetzt. async
funktioniert auch ohne await
(auch wenn das nicht wirklich sinnvoll ist), aber await
funktioniert ohne async
überhaupt nicht. Um await
innerhalb einer Methode zu verwenden, muss sie als async
gekennzeichnet sein.
Das await-Schlüsselwort
Hier ist ein Beispiel einer Methode, die await
verwendet:
Bis auf die Schlüsselwörter async
und await
und den seltsamen Rückgabewert sieht das genauso aus, wie eine synchrone Methode aussehen würde: Eine Webseite herunterladen und im Anschluss überprüfen, ob der angegebene reguläre Ausdruck auf den Inhalt der Webseite anzuwenden ist.
Und genau darum geht ist. Obwohl die Methode asynchron ist, sieht sie aus wie eine synchrone Methode. Nur dass sie im Gegensatz zur synchronen Version nicht den Aufrufer blockiert, bis sie fertig ist, nicht im Mouse-Click-Handler hängenbleibt und am wichtigsten: nicht den Rest der Anwendung einfriert.
Wie funktioniert das?
Beim Aufruf der Methode wird die Methode genauso ausgeführt wie jede andere Methode auch, bis der erste await
-Aufruf erreicht wird. An dieser Stelle gibt die Methode dem Aufrufer einen Task
zurück (nennen wir ihn den Methoden-Task), der Informationen über den weiteren Verlauf enthält. Sobald der von GetStringAsync()
zurückgegebene Task
abgeschlossen ist, wird der Rest der Methode ausgeführt. Hierbei sind alle lokalen Variablen wie gewohnt in ihrem vorherigen Zustand und verfügbar, genau wie in nicht-async
-Methoden. Wenn die Methode den Rest ihrer Arbei erledigt hat (das Anwenden des regulären Ausdrucks), setzt sie auf magische Weise das Result
des Methoden-Tasks auf den Wert, der im return
-Ausdruck steht und markiert den Task
als IsCompleted
.
Üblicherwerise werden async
-Methoden von anderen async
-Methoden via await
aufgerufen. Dadurch muss der Aufrufer der DoesWebContentMatchPatternAsync()
-Methode nichts ungewöhnliches machen, um an den Rückgabewert zu gelangen. Es reicht, await
vor den Aufruf zu schreiben.
Dies hat einen Kompilierungsfehler zur Folge, wenn die aufrufende Methode nicht ebenfalls async
ist.
Wie führe ich await ein?
In der Praxis sieht passiert dies öfter beim Einführen von async
/await
. Das Schema ist immer das gleiche:
- Ein erstes
await
in die Methode schreiben.
- Feststellen, dass die Methode nicht mehr kompiliert, weil sie nicht
async
ist
- Die Methode selbst
async
machen.
-
- Wenn der Rückgabewert
void
: Rückgabewert in Task
ändern.
- Wenn der Rückgabewert irgendein Typ
T
war: Rückgabewert in Task<T>
ändern.
- Mit dem Lieblings-Refactoring-Tool das Suffix
Async
zum Methodennamen hinzufügen.
- Ab Schritt 1 für den Aufrufer wiederholen.
Hierdurch breitet sich typischerweise async
/await
schnell im gesamten Projekt aus, was jedoch positiv zu sehen ist.
Aber was passiert mit Exceptions?
Die Magie von await
beinhaltet noch mehr. Ein weiterer aufwändiger Teil asynchroner Programmierung ist das Fangen und behandeln von Fehlern, die im Code auftreten, der im Hintergrund läuft. async
/await
löst dieses Problem auf elegante Weise durch weiterreichen von Exceptions über die Eigenschaften IsFaulted
and Exception
in der Task
-Klasse. Das bedeutet, dass man Exceptions genauso behandeln kann wie in synchronem Code.
Wird eine Exception nicht gefangen, wird sie, wie bei synchronem Code, an die aufrufende Methode weitergereicht. Solange der Code durchgängig async
/await
verwendet, muss nicht weiter geändert werden.
Das leidige Thema mit dem UI-Thread
Das letzte Stück Magie nennt sich Kontextsynchronisation. Ein beliebter Stolperstein asynchroner Entwicklung rührt aus der Tatsache, dass die meisten UI-Technologien einen einzelnen UI-Thread besitzen, der alle Änderungen am UI durchführen muss. Ändert man ein UI-Element aus einem anderen Thread, führt dies typischerweise zu einer Exception. Hierzu ein weiteres Beispiel, das den obigen Code verwendet:
Dieser Code zeigt einen Event-Handler eines Button-Click-Events im Code-Behind unter WPF (also dort, wo man solche Funktionalität typischerweise nicht umsetzen sollte). Die Zeile, die resultBox.Text
einen neuen Wert zuweist, ist eine Zeile, die unter WPF aus dem UI-Thread aufgerufen werden muss. Trotzdem steht hier kein Code, der diesen Aufruf an den UI-Kontext delegiert. Das liegt daran, dass das Standardverhalten von async
/await
so ist, dass es versucht, den Code hinter dem await
wieder im den Ursprungskontext auszuführen.
Dieses Standardverhalten wird typischerweise nur in UI-Code benötigt und sollte für alle andern Fälle deaktiviert werden, da es zusätzlichen Aufwand bedeutet und Deadlocks verursachen kann. Es lässt sich durch einen Aufruf der von ConfigureAwait(false)
auf den jeweiligen Task
deaktivieren:
Hierdurch wird dem Compiler mitgeteilt, dass es OK ist, den Rest der Methode (also alles hinter dem await
) im gleichen Kontext auszuführen, in dem auch der asynchrone Code hinter dem Schlüsselwort await
ausgeführt wurde.
Einfach ausprobieren!
Probiere es einfach mal async
/await
aus, wenn Dein Projekt schon .NET 4.5 unterstützt. Es wird Deine Wahrnehmung asynchroner Entwicklung nachhaltig verändern.