“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.

ZweckNicht verwendenVerwenden
Abrufen des Ergebnisses einer Hintergrund-TaskTask.Wait, Task.Resultawait
Warten auf das Abschließen einer TaskTask.WaitAnyawait Task.WhenAny
Abrufen der Ergebnisse mehrerer TasksTask.WaitAllawait Task.WhenAll
Warten über einen bestimmten ZeitraumThread.Sleepawait 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.