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?
Das funktioniert:
%Vor%Dieser Code wird blockiert:
%Vor%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
). 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.
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:
Wenn _client.GetStringAsync
beendet ist, kann der Rest des Codes nicht mehr im ursprünglichen Kontext fortgesetzt werden, da dieser Kontext blockiert ist.
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.
Wenn Sie weiterhin async
anstelle von ContinueWith
verwenden möchten, können Sie .ConfigureAwait(false)
zum ersten gefundenen async
hinzufügen:
Wie Sie wahrscheinlich schon wissen, teilt await
nicht, dass der restliche Code im ursprünglichen Kontext ausgeführt werden soll.
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.
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.
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%}
Tags und Links asp.net-mvc .net c# multithreading async-await