Ein asynchrones / wartendes Beispiel, das einen Deadlock verursacht

93

Ich bin auf einige Best Practices für die asynchrone Programmierung mit c # s async/ awaitkeywords gestoßen (ich bin neu in c # 5.0).

Einer der gegebenen Ratschläge war der folgende:

Stabilität: Kennen Sie Ihre Synchronisationskontexte

... Einige Synchronisationskontexte sind nicht wiedereintrittsfähig und Single-Threaded. Dies bedeutet, dass jeweils nur eine Arbeitseinheit im Kontext ausgeführt werden kann. Ein Beispiel hierfür ist der Windows-UI-Thread oder der ASP.NET-Anforderungskontext. In diesen Single-Threaded-Synchronisationskontexten ist es einfach, sich selbst zu blockieren. Wenn Sie eine Aufgabe aus einem Single-Thread-Kontext heraus erzeugen und dann im Kontext auf diese Aufgabe warten, blockiert Ihr Wartecode 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 zu einem neuen in MyWebService.GetDataAsync();, aber da der Haupt-Thread dort wartet, wartet er auf das Ergebnis in GetDataAsync().Result. In der Zwischenzeit sagen wir, die Daten sind fertig. Warum setzt der Hauptthread seine Fortsetzungslogik nicht fort und gibt ein Zeichenfolgenergebnis von zurück GetDataAsync()?

Kann mir bitte jemand erklären, warum es im obigen Beispiel einen Deadlock gibt? Ich habe keine Ahnung, wo das Problem liegt ...

Dror Weiss
quelle
Sind Sie wirklich sicher, dass GetDataAsync fertig ist? Oder bleibt es stecken und verursacht nur eine Sperre und keine Blockierung?
Andrey
Dies ist das Beispiel, das bereitgestellt wurde. Nach meinem Verständnis sollte es fertig sein und ein Ergebnis bereit haben ...
Dror Weiss
4
Warum wartest du überhaupt auf die Aufgabe? Sie sollten stattdessen warten, da Sie im Grunde alle Vorteile des asynchronen Modells verloren haben.
Toni Petrina
Das Hinzufügen zu @ ToniPetrinas Punkt, auch ohne das Deadlock-Problem, var data = GetDataAsync().Result;ist eine Codezeile, die niemals in einem Kontext ausgeführt werden sollte, den Sie nicht blockieren sollten (UI- oder ASP.NET-Anforderung). Selbst wenn es nicht blockiert, blockiert es den Thread auf unbestimmte Zeit. Im Grunde ist es ein schreckliches Beispiel. [Sie müssen den UI-Thread verlassen, bevor Sie solchen Code ausführen, oder awaitdort auch verwenden, wie Toni vorschlägt.]
ToolmakerSteve

Antworten:

81

Schauen Sie sich dieses Beispiel an , Stephen hat eine klare Antwort für Sie:

Dies geschieht also, beginnend mit der Methode der obersten Ebene ( Button1_Clickfür UI / MyController.Getfür ASP.NET):

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

  2. GetJsonAsyncStartet die REST-Anforderung durch Aufrufen HttpClient.GetStringAsync(immer noch im Kontext).

  3. GetStringAsyncGibt eine unvollständige TaskAnforderung zurück , die angibt, dass die REST-Anforderung nicht abgeschlossen ist.

  4. GetJsonAsyncwartet auf die TaskRückkehr von GetStringAsync. Der Kontext wird erfasst und verwendet, um die GetJsonAsyncMethode später fortzusetzen . GetJsonAsyncGibt eine unvollständige Methode zurück Task, die angibt, dass die GetJsonAsyncMethode nicht vollständig ist.

  5. Die Methode der obersten Ebene blockiert synchron das Taskzurückgegebene von GetJsonAsync. Dies blockiert den Kontext-Thread.

  6. ... Schließlich wird die REST-Anforderung abgeschlossen. Dies vervollständigt das Task, was von zurückgegeben wurde GetStringAsync.

  7. Die Fortsetzung für GetJsonAsyncist jetzt zur Ausführung bereit und wartet darauf, dass der Kontext verfügbar ist, damit er im Kontext ausgeführt werden kann.

  8. Deadlock . Die Methode der obersten Ebene blockiert den Kontext-Thread und wartet auf GetJsonAsyncden Abschluss und GetJsonAsyncdarauf, dass der Kontext frei ist, damit er abgeschlossen werden kann. Für das UI-Beispiel ist der "Kontext" der UI-Kontext. Für das ASP.NET-Beispiel ist der "Kontext" der ASP.NET-Anforderungskontext. Diese Art von Deadlock kann für jeden "Kontext" verursacht werden.

Ein weiterer Link, den Sie lesen sollten: Warten und Benutzeroberfläche und Deadlocks! Oh mein!

cuongle
quelle
19
  • Fakt 1: GetDataAsync().Result;Wird ausgeführt, wenn die von zurückgegebene Aufgabe GetDataAsync()abgeschlossen ist. In der Zwischenzeit wird der UI-Thread blockiert
  • Fakt 2: Die Fortsetzung von await ( return result.ToString()) wird zur Ausführung in den UI-Thread gestellt
  • Fakt 3: Die von zurückgegebene Aufgabe GetDataAsync()wird abgeschlossen, wenn die Fortsetzung in der Warteschlange ausgeführt wird
  • Fakt 4: Die Fortsetzung in der Warteschlange wird niemals ausgeführt, da der UI-Thread blockiert ist (Fakt 1).

Sackgasse!

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

  • Vermeiden Sie 1,4. Anstatt den UI-Thread zu blockieren, verwenden Sie var data = await GetDataAsync(), damit der UI-Thread weiter ausgeführt werden kann
  • Vermeiden Sie 2,3. Stellen Sie die Fortsetzung des Wartens in eine Warteschlange für einen anderen Thread, der nicht blockiert ist, z. B. use var data = Task.Run(GetDataAsync).Result, wodurch die Fortsetzung in den Synchronisierungskontext eines Threadpool-Threads gestellt wird. Dadurch kann die von zurückgegebene Aufgabe GetDataAsync()abgeschlossen werden.

Dies wird in einem Artikel von Stephen Toub sehr gut erklärt , ungefähr auf halber Strecke, wo er das Beispiel von verwendet DelayAsync().

Phillip Ngan
quelle
In Bezug auf var data = Task.Run(GetDataAsync).Resultdas ist, neu für mich. Ich habe immer gedacht, dass das Äußere .Resultsofort verfügbar sein wird, sobald das erste Warten auf GetDataAsyncgetroffen wird, also datawird es immer so sein default. Interessant.
Nawfal
18

Ich habe gerade in einem ASP.NET MVC-Projekt wieder an diesem Problem herumgespielt. Wenn Sie asyncMethoden von a aufrufen möchten PartialView, dürfen Sie die nicht erstellen PartialView async. Sie erhalten eine Ausnahme, wenn Sie dies tun.

Sie können die folgende einfache Problemumgehung in dem Szenario verwenden, in dem Sie eine asyncMethode aus einer Synchronisierungsmethode aufrufen möchten :

  1. Löschen Sie vor dem Anruf das SynchronizationContext
  2. Machen Sie den Anruf, es wird hier keinen Deadlock mehr geben, warten Sie, bis er beendet ist
  3. Stellen Sie die wieder her SynchronizationContext

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);
}
Herre Kuijpers
quelle
3

Ein weiterer wichtiger Punkt ist, dass Sie Aufgaben nicht blockieren und asynchron 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();
}
MarvelTracker
quelle
6
Was passiert , wenn ich will die Haupt (UI) Thread , bis die Aufgabe beendet werden blockiert? Oder zum Beispiel in einer Konsolen-App? Angenommen, ich möchte HttpClient verwenden, das nur Async unterstützt ... Wie verwende ich es synchron ohne das Risiko eines Deadlocks ? Das muss möglich sein. Wenn WebClient auf diese Weise verwendet werden kann (aufgrund von Synchronisierungsmethoden) und einwandfrei funktioniert, warum kann dies dann nicht auch mit HttpClient durchgeführt werden?
Dexter
Siehe Antwort von Philip Ngan oben (ich weiß, dass dies nach diesem Kommentar gepostet wurde): Stellt die Fortsetzung des Wartens in einen anderen Thread, der nicht blockiert ist, in eine Warteschlange, z. B. verwende var data = Task.Run (GetDataAsync) .Result
Jeroen
@Dexter - re " Was ist, wenn der Hauptthread (UI) blockiert werden soll, bis die Aufgabe abgeschlossen ist? " - Möchten Sie wirklich, dass der UI-Thread blockiert wird, was bedeutet, dass der Benutzer nichts tun kann, nicht einmal abbrechen kann - oder ist Ist es so, dass Sie die Methode, in der Sie sich befinden, nicht fortsetzen möchten? "warte" oder "Task.ContinueWith" behandelt den letzteren Fall.
ToolmakerSteve
@ToolmakerSteve natürlich möchte ich die Methode nicht fortsetzen. Aber ich kann wait einfach nicht verwenden, da ich async auch nicht vollständig verwenden kann - HttpClient wird in main aufgerufen , was natürlich nicht async sein kann. Und dann habe ich erwähnt, dass ich das alles in einer Konsolen-App mache - in diesem Fall möchte ich genau die erstere - ich möchte nicht, dass meine App überhaupt ein Multithreading ist. Blockiere alles .
Dexter
-1

Eine Lösung, zu der ich gekommen bin, besteht darin, eine JoinErweiterungsmethode für die Aufgabe zu verwenden, bevor ich nach dem Ergebnis frage.

Der Code sieht folgendermaßen aus:

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

  return View(data);
}

Wo die Join-Methode ist:

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)

Orace
quelle