Inhaltsverzeichnis
“Durchgehend asynchron” bedeutet, dass man synchronen und asynchronen Code nicht mischen sollten, ohne gründlich über die Folgen gedacht zu haben. Es ist eine schlechte Idee, asynchronen Code durch Aufrufen von Task.Wait
oder Task.Result
zu blockieren.
Das Problem taucht häufig auf, wenn Entwicklern nur einen kleinen Teil ihrer Anwendung konvertieren und diesen in eine synchrone API einschließen. So bleibt der Rest der Anwendungen von den Änderungen isoliert. Irgendwann taucht unweigerlich die Frage auf: Warum treten bei meinem teilweise asynchronen Code Deadlocks auf?
Blockieren von asynchronem Code
Ein Beispiel, bei dem eine Methode das Ergebnis einer async-Methode blockiert. Dieser Code funktioniert gut in einer Konsolenanwendung, erzeugt aber einen Deadlock, wenn er über einen GUI- oder ASP.NET-Kontext aufgerufen wird. Die Ursache des Deadlocks liegt in der Aufrufliste, beim Aufruf von Task.Wait
.
public static class DeadlockExample { // This method causes a deadlock, // when called in a GUI or ASP.NET context. public static void Test() { // Start the delay. var delayTask = DelayAsync(); // Wait for the delay to complete. delayTask.Wait(); } private static async Task DelayAsync() { await Task.Delay(1000); } }
Die Ursache dieses Deadlocks liegt in der Art und Weise, wie das await
“Contexts” verarbeitet. Beim Warten auf eine unvollständige Task
wird standardmäßig der aktuelle context
erfasst und zum Fortsetzen der Methode verwendet, wenn die Task
abgeschlossen ist. Dieser context
ist der aktuelle SynchronizationContext
, sofern er nicht NULL
ist; ansonsten ist es der aktuelle TaskScheduler
. GUI- und ASP.NET-Anwendungen haben einen SynchronizationContext
, der die Ausführung immer nur eines Codeteils zulässt. Wenn das await
abgeschlossen wurde, versucht es, den Rest der async-Methode im erfassten context
auszuführen. Dieser context
enthält aber bereits einen Thread, der (synchron) darauf wartet, dass die async-Methode abgeschlossen wird. Sie warten aufeinander und verursachen so einen Deadlock.
Bei Konsolenanwendungen tritt dieser Deadlock nicht auf, weil diese über einen Threadpool-SynchronizationContext
anstatt des SynchronizationContext
, der immer nur einen Codeteil zulässt, verfügen.
Die Lösung für dieses Problem ist, das natürliche Ausbreiten des asynchronen Codes in der Codebasis nicht zu unterbinden. Der asynchrone Code muss sich bis zu seinem Einstiegspunkt ausbreiten, in der Regel ein EventHandler
oder eine Action
.
blockierender Code innerhalb einer async-Methode
Es gibt noch ein weiteres Problem, wenn blockierender Code innerhalb einer async-Methode verwendet wird.
public static class NotFullyAsynchronousDemo { // This method synchronously blocks a thread. public static async Task TestNotFullyAsync() { await Task.Yield(); Thread.Sleep(5000); } }
Diese Methode ist nicht vollständig asynchron. Sie gibt sofort eine unvollständige Task
zurück, blockiert aber im weiteren Verlauf jedweden laufenden Thread synchron.
- Wird diese Methode von einem GUI-Kontext aufgerufen, blockiert sie den GUI-Thread.
- Wird sie von einem ASP.NET-Anforderungskontext aufgerufen, blockiert sie den aktuellen ASP.NET-Anforderungsthread.
Asynchroner Code funktioniert am besten, wenn er nicht synchron blockiert.
asynchrone Ansatz
Leitfaden für asynchrone Ersetzungen in synchronen Operationen.
Zweck | Nicht verwenden | Verwenden |
Abrufen des Ergebnisses einer Hintergrund-Task | Task.Wait , Task.Result | await |
Warten auf das Abschließen einer Task | Task.WaitAny | await Task.WhenAny |
Abrufen der Ergebnisse mehrerer Tasks | Task.WaitAll | await Task.WhenAll |
Warten über einen bestimmten Zeitraum | Thread.Sleep | await Task.Delay |
Zusammenfassung
Man sollte asynchronen und blockierenden Code nicht miteinander mischen. Gemischter asynchroner und blockierender Code kann zu Deadlocks, komplexer Fehlerbehandlung und unerwartetem Blockieren von Kontextthreads führen.