HttpClient.GetAsync (…) kehrt nie zurück, wenn “Warten / Asynchron” verwendet wird

Bearbeiten: Diese Frage sieht aus wie das gleiche Problem, aber hat keine Antworten …

Edit: Im Testfall 5 scheint die Aufgabe im WaitingForActivation zu WaitingForActivation .

Ich habe einige seltsame Verhalten mit dem System.Net.Http.HttpClient in .NET 4.5 aufgetreten – wo “warten” auf das Ergebnis eines Aufrufs an (zB) httpClient.GetAsync(...) wird nie zurückkehren.

Dies tritt nur unter bestimmten Umständen auf, wenn die neue async / appear-Sprachfunktionalität und die Tasks-API verwendet werden – der Code scheint immer zu funktionieren, wenn nur Fortsetzungen verwendet werden.

Hier ist ein Code, der das Problem reproduziert: Legen Sie dieses in einem neuen “MVC 4 WebApi-Projekt” in Visual Studio 11 ab, um die folgenden GET-Endpunkte verfügbar zu machen:

 /api/test1 /api/test2 /api/test3 /api/test4 /api/test5 <--- never completes /api/test6 

Jeder der Endpunkte gibt die gleichen Daten zurück (die Antwortheader von stackoverflow.com), mit Ausnahme von /api/test5 die niemals abgeschlossen wird.

Bin ich in der HttpClient-class auf einen Fehler gestoßen oder missbrauche ich die API irgendwie?

Code zum Reproduzieren:

 public class BaseApiController : ApiController { ///  /// Retrieves data using continuations ///  protected Task Continuations_GetSomeDataAsync() { var httpClient = new HttpClient(); var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString()); } ///  /// Retrieves data using async/await ///  protected async Task AsyncAwait_GetSomeDataAsync() { var httpClient = new HttpClient(); var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead); return result.Content.Headers.ToString(); } } public class Test1Controller : BaseApiController { ///  /// Handles task using Async/Await ///  public async Task Get() { var data = await Continuations_GetSomeDataAsync(); return data; } } public class Test2Controller : BaseApiController { ///  /// Handles task by blocking the thread until the task completes ///  public string Get() { var task = Continuations_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test3Controller : BaseApiController { ///  /// Passes the task back to the controller host ///  public Task Get() { return Continuations_GetSomeDataAsync(); } } public class Test4Controller : BaseApiController { ///  /// Handles task using Async/Await ///  public async Task Get() { var data = await AsyncAwait_GetSomeDataAsync(); return data; } } public class Test5Controller : BaseApiController { ///  /// Handles task by blocking the thread until the task completes ///  public string Get() { var task = AsyncAwait_GetSomeDataAsync(); var data = task.GetAwaiter().GetResult(); return data; } } public class Test6Controller : BaseApiController { ///  /// Passes the task back to the controller host ///  public Task Get() { return AsyncAwait_GetSomeDataAsync(); } } 

   

Sie missbrauchen die API.

Hier ist die Situation: In ASP.NET kann jeweils nur ein Thread eine Anfrage bearbeiten. Sie können bei Bedarf eine parallele Verarbeitung durchführen (zusätzliche Threads aus dem Thread-Pool leihen), aber nur ein Thread hat den Anforderungskontext (die zusätzlichen Threads haben keinen Anforderungskontext).

Dies wird vom ASP.NET- SynchronizationContext .

Wenn Sie await einen Task await , wird die Methode standardmäßig bei einem erfassten SynchronizationContext (oder einem erfassten TaskScheduler , wenn kein SynchronizationContext ) TaskScheduler . Normalerweise ist dies genau das, was Sie wollen: Eine asynchrone Controller-Aktion await etwas, und wenn es fortgesetzt wird, wird es mit dem Anforderungskontext fortgesetzt.

Also, hier ist warum test5 fehlschlägt:

  • Test5Controller.Get führt AsyncAwait_GetSomeDataAsync (innerhalb des ASP.NET-Anforderungskontexts) aus.
  • AsyncAwait_GetSomeDataAsync führt HttpClient.GetAsync (innerhalb des ASP.NET-Anforderungskontexts) aus.
  • Die HTTP-Anforderung wird gesendet, und HttpClient.GetAsync gibt eine unvollständige Task .
  • AsyncAwait_GetSomeDataAsync erwartet die Task . Da es nicht abgeschlossen ist, gibt AsyncAwait_GetSomeDataAsync eine unvollständige Task .
  • Test5Controller.Get blockiert den aktuellen Thread, bis diese Task abgeschlossen ist.
  • Die HTTP-Antwort kommt herein und die von HttpClient.GetAsync Task ist abgeschlossen.
  • AsyncAwait_GetSomeDataAsync versucht, den ASP.NET-Anforderungskontext AsyncAwait_GetSomeDataAsync . In diesem Kontext befindet sich jedoch bereits ein Thread: der in Test5Controller.Get blockierte Test5Controller.Get .
  • Sackgasse.

Hier ist, warum die anderen arbeiten:

  • ( test1 , test2 und test3 ): Continuations_GetSomeDataAsync plant die Fortsetzung des test3 außerhalb des ASP.NET-Anforderungskontexts. Dadurch kann die von Continuations_GetSomeDataAsync Task abgeschlossen werden, ohne dass der Anfragekontext erneut eingegeben werden muss.
  • ( test4 und test6 ): Da der Task erwartet wird , wird der ASP.NET-Anforderungs-Thread nicht blockiert. Dadurch kann AsyncAwait_GetSomeDataAsync den ASP.NET-Anforderungskontext verwenden, wenn der AsyncAwait_GetSomeDataAsync fortgesetzt werden soll.

Und hier sind die besten Praktiken:

  1. Verwenden Sie in Ihren async “Bibliotheks” -Methoden ConfigureAwait(false) wann immer dies möglich ist. In Ihrem Fall würde dies AsyncAwait_GetSomeDataAsync ändern, um var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Blockiere nicht auf Task s; Es ist async ganzen Weg hinunter. Mit anderen Worten, verwenden Sie anstelle von GetResult await ( Task.Result und Task.Wait sollten auch durch await ).

Auf diese Weise erhalten Sie beide Vorteile: Die Fortsetzung (der Rest der AsyncAwait_GetSomeDataAsync Methode) wird auf einem grundlegenden AsyncAwait_GetSomeDataAsync Thread ausgeführt, der den ASP.NET-Anforderungskontext nicht eingeben muss. und der Controller selbst ist async (was einen Anfrage-Thread nicht blockiert).

Mehr Informationen:

  • Mein async / await Intro-Post , der eine kurze Beschreibung der Verwendung von SynchronizationContext .
  • Die Async / Await FAQ , die auf die Kontexte näher eingeht . Siehe auch Warten und UI und Deadlocks! Oh mein! Dies gilt auch dann, wenn Sie sich in ASP.NET und nicht in einer Benutzeroberfläche befinden, da der ASP.NET- SynchronizationContext den Anforderungskontext auf jeweils nur einen Thread beschränkt.
  • Dieses MSDN Forum posten .
  • Stephen Toub demonstriert diesen Deadlock (mit einer UI) und Lucian Wischik ebenfalls .

Update 2012-07-13: Diese Antwort wurde in einen Blogpost integriert .

Edit: Im Allgemeinen versuchen Sie, die folgenden Schritte zu vermeiden, außer als letzten Versuch, Deadlocks zu vermeiden. Lesen Sie den ersten Kommentar von Stephen Cleary.

Schnelle Lösung von hier . Anstatt zu schreiben:

 Task tsk = AsyncOperation(); tsk.Wait(); 

Versuchen:

 Task.Run(() => AsyncOperation()).Wait(); 

Oder wenn Sie ein Ergebnis benötigen:

 var result = Task.Run(() => AsyncOperation()).Result; 

Von der Quelle (bearbeitet, um dem obigen Beispiel zu entsprechen):

AsyncOperation wird jetzt im ThreadPool aufgerufen, wo kein SynchronizationContext vorhanden ist und die in AsyncOperation verwendeten Fortsetzungen nicht zurück in den aufrufenden Thread erzwungen werden.

Für mich sieht das wie eine brauchbare Option aus, da ich nicht die Möglichkeit habe, es asynchron zu machen (was ich bevorzugen würde).

Von der Quelle:

Stellen Sie sicher, dass das Warten in der FooAsync-Methode keinen Kontext findet, zu dem Sie zurückmarshallen können. Der einfachste Weg, dies zu tun, besteht darin, die asynchrone Arbeit vom ThreadPool aus aufzurufen, z. B. indem der Aufruf in eine Task.Run eingeschlossen wird, z

int Sync () {Zurückgeben von Task.Run (() => Library.FooAsync ()). }

FooAsync wird jetzt im ThreadPool aufgerufen, wo kein SynchronizationContext vorhanden ist und die Fortsetzungen, die in FooAsync verwendet werden, nicht zurück zu dem Thread gezwungen werden, der Sync () aufruft.

Da Sie .Result oder .Wait oder await dies zu einem Deadlock in Ihrem Code.

Sie können ConfigureAwait(false) in async Methoden verwenden, um Deadlocks zu verhindern

so was:

 var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); 

Sie können ConfigureAwait(false) wo immer möglich verwenden, um Async-Code nicht zu blockieren.

Diese beiden Schulen schließen nicht wirklich aus.

Hier ist das Szenario, wo Sie einfach verwenden müssen

  Task.Run(() => AsyncOperation()).Wait(); 

oder so ähnlich

  AsyncContext.Run(AsyncOperation); 

Ich habe eine MVC-Aktion, die unter database Transaktionsattribut ist. Die Idee war (wahrscheinlich), alles, was in der Aktion gemacht wurde, rückgängig zu machen, wenn etwas schief geht. Dies erlaubt keine Kontextumschaltung, da sonst ein Rollback oder Commit der Transaktion selbst fehlschlägt.

Die Bibliothek, die ich brauche, ist asynchron, da Async erwartet wird.

Die einzige Option. Führen Sie es als normalen Synchronisierungsanruf aus.

Ich sage nur jedem etwas.

Ich suche hier:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx

Und hier:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

Und sehen:

Dieser Typ und seine Member sind für den Compiler vorgesehen.

Wenn man bedenkt, dass die ” await version” funktioniert und “richtig” ist, braucht man wirklich eine Antwort auf diese Frage?

Meine Stimme ist: Missbrauch der API .