web-dev-qa-db-de.com

Ein async/await-Beispiel, das zu einem Deadlock führt

Ich habe einige Best Practices für die asynchrone Programmierung mit den async/await-Schlüsselwörtern von c # gefunden (ich bin neu in c # 5.0).

Einer der Ratschläge war der folgende:

Stabilität: Kennen Sie Ihre Synchronisationskontexte

... Einige Synchronisierungskontexte sind nicht wiedereintrittsfähig und Singlethread. Dies bedeutet, dass jeweils nur eine Arbeitseinheit im Kontext ausgeführt werden kann. Ein Beispiel dafür ist der Windows-UI-Thread oder der ASP.NET-Anforderungskontext ..__ In diesen Kontexten für die Synchronisierung mit einem einzigen Thread können Sie sich leicht selbst blockieren. Wenn Sie eine Aufgabe aus einem Einzelthread-Kontext erzeugen und dann auf diese Aufgabe im Kontext warten, blockiert der wartende Code möglicherweise die Hintergrundaufgabe.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

Wenn ich versuche, es selbst zu zerlegen, wird der Haupt-Thread in MyWebService.GetDataAsync(); zu einem neuen Thread, aber da der Haupt-Thread dort wartet, wartet er auf das Ergebnis in GetDataAsync().Result. Inzwischen sagen wir, die Daten sind fertig. Warum setzt der Haupt-Thread nicht seine Fortführungslogik fort und gibt ein String-Ergebnis von GetDataAsync() zurück?

Kann mir bitte jemand erklären, warum es im obigen Beispiel einen Deadlock gibt? Ich bin völlig ahnungslos, was das Problem ist ...

79
Dror Weiss

Schauen Sie sich ein Beispiel in hier an, Stephen hat eine klare Antwort für Sie:

Das passiert also, beginnend mit der Top-Level-Methode (Button1_Click für UI/MyController.Get für ASP.NET):

  1. Die Methode der obersten Ebene ruft GetJsonAsync (im UI/ASP.NET-Kontext) auf.

  2. GetJsonAsync startet die Anforderung REST durch Aufruf von HttpClient.GetStringAsync (immer noch im Kontext).

  3. GetStringAsync gibt eine nicht abgeschlossene Task zurück, die angibt, dass die Anforderung REST nicht abgeschlossen ist.

  4. GetJsonAsync erwartet die von GetStringAsync zurückgegebene Aufgabe. Der Kontext wird erfasst und verwendet, um die Ausführung von .__ fortzusetzen. GetJsonAsync-Methode später. GetJsonAsync gibt eine nicht abgeschlossene Aufgabe zurück Gibt an, dass die GetJsonAsync-Methode nicht abgeschlossen ist.

  5. Die Methode der obersten Ebene blockiert synchron die von GetJsonAsync zurückgegebene Task. Dies blockiert den Kontext-Thread.

  6. ... Die Anforderung REST wird schließlich abgeschlossen. Damit ist die von GetStringAsync zurückgegebene Aufgabe abgeschlossen.

  7. Die Fortsetzung für GetJsonAsync kann jetzt ausgeführt werden und wartet, bis der Kontext verfügbar ist, damit er im Kontext ausgeführt werden kann.

  8. Sackgasse. Die Methode der obersten Ebene blockiert den Kontext-Thread und wartet, bis GetJsonAsync abgeschlossen ist. GetJsonAsync wartet auf Der Kontext muss frei sein, damit er abgeschlossen werden kann. Für das Beispiel der Benutzeroberfläche ist der "Kontext" ist der UI-Kontext; Für das ASP.NET-Beispiel lautet der "Kontext" den ASP.NET-Anforderungskontext. Diese Art von Deadlock kann für .__ verursacht werden. entweder "Kontext".

Ein weiterer Link, den Sie lesen sollten:

Await und UI und Deadlocks! Oh mein!

68
cuongle
  • Fakt 1: GetDataAsync().Result; wird ausgeführt, wenn die von GetDataAsync() zurückgegebene Aufgabe abgeschlossen ist. In der Zwischenzeit blockiert sie den UI-Thread
  • Fakt 2: Die Fortsetzung des Erwarten (return result.ToString()) wird zur Ausführung in den UI-Thread aufgenommen
  • Fakt 3: Die von GetDataAsync() zurückgegebene Aufgabe wird abgeschlossen, wenn die Fortsetzung in der Warteschlange ausgeführt wird
  • Fakt 4: Die fortlaufende Warteschlange wird nie ausgeführt, da der UI-Thread blockiert ist (Fakt 1).

Sackgasse!

Der Deadlock kann durch bereitgestellte Alternativen aufgehoben werden, um Fact 1 oder Fact 2 zu vermeiden.

  • Vermeiden Sie 1,4. Verwenden Sie anstelle des UI-Threads den Befehl var data = await GetDataAsync(), wodurch der UI-Thread weiter ausgeführt werden kann
  • Vermeiden Sie 2,3. Schlagen Sie die Fortsetzung des Warten auf einen anderen Thread ein, der nicht blockiert ist, z. Verwenden Sie var data = Task.Run(GetDataAsync).Result, um die Fortsetzung im Sync-Kontext eines Threadpool-Threads zu buchen. Dadurch kann die von GetDataAsync() zurückgegebene Aufgabe abgeschlossen werden. 

Dies wird in einem Artikel von Stephen Toub sehr gut erklärt, etwa in der Mitte, wo er das Beispiel von DelayAsync() verwendet.

11
Phillip Ngan

Ich habe gerade wieder in einem MVC.Net-Projekt mit diesem Problem herumgepfuscht. Wenn Sie async-Methoden aus einer PartialView aufrufen möchten, dürfen Sie die PartialView nicht asynchron machen. Sie erhalten eine Ausnahme, wenn Sie dies tun.

Im Grunde eine einfache Problemumgehung in dem Szenario, in dem Sie eine async-Methode von einer sync-Methode aufrufen möchten, können Sie Folgendes tun:

  1. löschen Sie vor dem Aufruf den SynchronizationContext
  2. machen Sie den Anruf, hier wird es keinen Deadlock mehr geben, warten Sie, bis es beendet ist
  3. stellen Sie den SynchronizationContext wieder her

Beispiel:

    public ActionResult DisplayUserInfo(string userName)
    {
        // trick to prevent deadlocks of calling async method 
        // and waiting for on a sync UI thread.
        var syncContext = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(null);

        //  this is the async call, wait for the result (!)
        var model = _asyncService.GetUserInfo(Username).Result;

        // restore the context
        SynchronizationContext.SetSynchronizationContext(syncContext);

        return PartialView("_UserInfo", model);
    }
10
Herre Kuijpers

Ein weiterer wichtiger Punkt ist, dass Sie Aufgaben nicht blockieren und async ganz nach unten verwenden sollten, um Deadlocks zu vermeiden. Dann wird alles asynchron und nicht synchron blockiert.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}
2
marvelTracker

Eine Problemumgehung war, eine Join-Erweiterungsmethode für die Aufgabe zu verwenden, bevor nach dem Ergebnis gefragt wird.

Der Code sieht so aus:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Wo ist die Join-Methode:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

Ich bin nicht genug in der Domäne, um die Nachteile dieser Lösung zu sehen (falls vorhanden)

0
Orace