Unterschiedliches Verhalten bei Verwendung von ContinueWith oder Async-Await

7

Wenn ich in einem HttpClient-Aufruf eine async-await-Methode (wie im Beispiel unten) verwende, verursacht dieser Code einen Deadlock . Wenn Sie die async-await-Methode durch t.ContinueWith ersetzen, funktioniert sie ordnungsgemäß. Warum?

%Vor%

Das funktioniert:

%Vor%

Dieser Code wird blockiert:

%Vor%     
Douglas H. M. 08.06.2013, 05:23
quelle

4 Antworten

9

Ich beschreibe dieses Deadlockverhalten in meinem Blog und in einem aktuellen MSDN-Artikel .

  • await plant standardmäßig, dass seine Fortsetzung in der aktuellen SynchronizationContext ausgeführt wird, oder (falls keine SynchronizationContext vorhanden ist) der aktuellen TaskScheduler . (In diesem Fall ist die ASP.NET-Anfrage SynchronizationContext ).
  • Der ASP.NET SynchronizationContext stellt den Anforderungskontext dar, und ASP.NET lässt nur jeweils einen Thread in diesem Kontext zu.

Wenn die HTTP-Anfrage abgeschlossen ist, versucht sie also, SynchronizationContext einzugeben, um InfoFormat auszuführen. Es gibt jedoch bereits einen Thread in SynchronizationContext - der Block, der auf Result blockiert wurde (Warten auf die Methode async ).

Auf der anderen Seite wird das Standardverhalten für ContinueWith standardmäßig seine Fortsetzung auf die aktuelle TaskScheduler (was in diesem Fall der Threadpool TaskScheduler ist) einplanen.

Wie andere bemerkt haben, ist es am besten, await "den ganzen Weg" zu verwenden, d. h. nicht auf async code zu blockieren. Leider ist das in diesem Fall keine Option, da MVC asynchrone Aktionsfilter nicht unterstützt (Als Randnotiz bitte hier für die Unterstützung stimmen ).

Sie können also ConfigureAwait(false) verwenden oder nur synchrone Methoden verwenden. In diesem Fall empfehle ich synchrone Methoden. ConfigureAwait(false) funktioniert nur, wenn die Task , auf die sie angewendet wird, noch nicht abgeschlossen ist. Daher empfehle ich, dass Sie nach der Verwendung von ConfigureAwait(false) für jedes await in der Methode nach diesem Punkt verwenden (und in diesem Fall in jeder Methode in der Aufrufliste). Wenn ConfigureAwait(false) aus Effizienzgründen verwendet wird, ist das in Ordnung (weil es technisch optional ist). In diesem Fall wäre ConfigureAwait(false) aus Korrektheitsgründen notwendig, damit IMO einen Wartungsaufwand verursacht. Synchrone Methoden wären klarer.

    
Stephen Cleary 08.06.2013, 13:46
quelle
7

Eine Erklärung, warum Ihre Deadlocks warten

Ihre erste Zeile:

%Vor%

blockiert diesen Thread und den aktuellen Kontext, während er auf das Ergebnis von GetUserAsync wartet.

Bei der Verwendung von await wird versucht, alle verbleibenden Anweisungen im ursprünglichen Kontext nach Beendigung der Aufgabe erneut auszuführen, was zu Deadlocks führt, wenn der ursprüngliche Kontext blockiert wird (was auf .Result zurückzuführen ist). Es sieht so aus, als hätten Sie versucht, dieses Problem zu verhindern, indem Sie .ConfigureAwait(false) in GetUserAsync verwenden. Wenn diese await jedoch in Kraft ist, ist es zu spät, weil ein anderes await zuerst angetroffen wird. Der tatsächliche Ausführungspfad sieht folgendermaßen aus:

%Vor%

Wenn _client.GetStringAsync beendet ist, kann der Rest des Codes nicht mehr im ursprünglichen Kontext fortgesetzt werden, da dieser Kontext blockiert ist.

Warum ContinseWith verhält sich anders

ContinueWith versucht nicht, den anderen Block im ursprünglichen Kontext auszuführen (es sei denn, Sie sagen es mit einem zusätzlichen Parameter) und leidet daher nicht unter diesem Problem.

Dies ist der Unterschied im Verhalten, den Sie bemerkt haben.

Eine Lösung mit asynchronen

Wenn Sie weiterhin async anstelle von ContinueWith verwenden möchten, können Sie .ConfigureAwait(false) zum ersten gefundenen async hinzufügen:

%Vor%

Wie Sie wahrscheinlich schon wissen, teilt await nicht, dass der restliche Code im ursprünglichen Kontext ausgeführt werden soll.

Hinweis für die Zukunft

Versuchen Sie nach Möglichkeit, keine blockierenden Methoden zu verwenden, wenn Sie async / await verwenden. Siehe Verhindern eines Deadlocks beim Aufruf Eine asynchrone Methode ohne Verwendung von um dies in der Zukunft zu vermeiden.

    
FriendlyGuy 08.06.2013 06:02
quelle
2

Zugegeben, meine Antwort ist nur teilweise, aber ich werde trotzdem weitermachen.

Ihr Task.ContinueWith(...) -Aufruf spezifiziert nicht den Scheduler, deshalb wird TaskScheduler.Current verwendet - was auch immer das zu der Zeit ist. Ihr await -Snippet wird jedoch im abgefangenen Kontext ausgeführt, wenn die erwartete Aufgabe abgeschlossen ist, so dass die zwei Bits des Codes ein ähnliches Verhalten erzeugen können oder auch nicht - abhängig vom Wert von TaskScheduler.Current .

Wenn beispielsweise Ihr erstes Snippet direkt aus dem UI-Code aufgerufen wird (in diesem Fall TaskScheduler.Current == TaskScheduler.Default , wird die Fortsetzung (Logging-Code) auf dem Standard TaskScheduler ausgeführt, dh im Thread-Pool.

Im zweiten Snippet wird die Fortführung (Protokollierung) jedoch tatsächlich auf dem UI-Thread ausgeführt, unabhängig davon, ob Sie ConfigureAwait(false) für die von GetStringAsync zurückgegebene Task verwenden oder nicht. ConfigureAwait(false) beeinflusst nur die Ausführung des Codes , nachdem der Aufruf von GetStringAsync erwartet wird.

Hier ist noch etwas, um dies zu veranschaulichen:

%Vor%

Der angegebene Code setzt den Text innerhalb von Blah () in Ordnung, aber er löst eine Cross-Threading-Ausnahme innerhalb der Fortsetzung im Load-Handler aus.

    
Kirill Shlenskiy 08.06.2013 05:59
quelle
0

Ich fand die anderen Lösungen, die hier gepostet wurden, funktionierte nicht für mich auf ASP.NET MVC 5, das weiterhin synchrone Aktionsfilter verwendet. Die geposteten Lösungen garantieren nicht, dass ein neuer Thread verwendet wird, sie geben nur an, dass derselbe Thread nicht verwendet werden muss.

Meine Lösung besteht darin, Task.Factory.StartNew () zu verwenden und TaskCreationOptions.LongRunning im Methodenaufruf anzugeben. Dadurch wird sichergestellt, dass immer ein neuer / anderer Thread verwendet wird. So können Sie sicher sein, dass Sie niemals einen Deadlock erhalten.

Also, mit dem OP-Beispiel ist das Folgende die Lösung, die für mich funktioniert:

%Vor%

}

    
mixja 29.12.2013 00:56
quelle