HttpClient.GetAsync (…) kehrt bei Verwendung von await / async niemals zurück

314

Bearbeiten: Diese Frage scheint das gleiche Problem zu sein, hat aber keine Antworten ...

Bearbeiten: In Testfall 5 scheint die Aufgabe im WaitingForActivationZustand zu stecken .

Ich habe ein seltsames Verhalten bei der Verwendung des System.Net.Http.HttpClient in .NET 4.5 festgestellt, bei dem das "Warten" auf das Ergebnis eines Aufrufs von (z. B.) httpClient.GetAsync(...)niemals zurückkehrt.

Dies tritt nur unter bestimmten Umständen auf, wenn die neue Sprachfunktionalität async / await 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 diesen 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 hier gibt dieselben Daten zurück (die Antwortheader von stackoverflow.com), außer dass /api/test5diese niemals abgeschlossen werden.

Habe ich einen Fehler in der HttpClient-Klasse festgestellt oder missbrauche ich die API auf irgendeine Weise?

Zu reproduzierender Code:

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> 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
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}
Benjamin Fox
quelle
2
Es scheint nicht dasselbe Problem zu sein, aber nur um sicherzugehen, dass Sie darüber Bescheid wissen, gibt es einen MVC4-Fehler in den asynchronen Beta-WRT-Methoden, die synchron abgeschlossen werden - siehe stackoverflow.com/questions/9627329/…
James Manning,
Danke - ich werde darauf achten. In diesem Fall denke ich, dass die Methode wegen des Aufrufs von HttpClient.GetAsync(...)? Immer asynchron sein sollte .
Benjamin Fox

Antworten:

467

Sie missbrauchen die API.

Hier ist die Situation: In ASP.NET kann jeweils nur ein Thread eine Anforderung verarbeiten. Sie können bei Bedarf eine parallele Verarbeitung durchführen (zusätzliche Threads aus dem Thread-Pool ausleihen), aber nur ein Thread würde den Anforderungskontext haben (die zusätzlichen Threads haben nicht den Anforderungskontext).

Dies wird von ASP.NET verwaltetSynchronizationContext .

Wenn Sie awaita verwenden Task, wird die Methode standardmäßig für eine erfasste SynchronizationContext(oder eine erfasste TaskScheduler, wenn keine vorhanden ist) fortgesetzt SynchronizationContext. Normalerweise ist dies genau das, was Sie wollen: Eine asynchrone Controller-Aktion wird awaitetwas bewirken, und wenn sie fortgesetzt wird, wird sie mit dem Anforderungskontext fortgesetzt.

Hier ist der Grund, warum dies test5fehlschlägt:

  • Test5Controller.Getwird ausgeführt AsyncAwait_GetSomeDataAsync(im ASP.NET-Anforderungskontext).
  • AsyncAwait_GetSomeDataAsyncwird ausgeführt HttpClient.GetAsync(im ASP.NET-Anforderungskontext).
  • Die HTTP-Anforderung wird gesendet und HttpClient.GetAsyncgibt eine unvollständige zurück Task.
  • AsyncAwait_GetSomeDataAsyncerwartet das Task; da es nicht vollständig ist, wird AsyncAwait_GetSomeDataAsynceine unvollständige zurückgegeben Task.
  • Test5Controller.Get blockiert den aktuellen Thread, bis dieser Taskabgeschlossen ist.
  • Die HTTP-Antwort kommt herein und die TaskRückgabe von HttpClient.GetAsyncist abgeschlossen.
  • AsyncAwait_GetSomeDataAsyncVersuche, im ASP.NET-Anforderungskontext fortzufahren. In diesem Kontext gibt es jedoch bereits einen Thread: Der Thread ist blockiert Test5Controller.Get.
  • Sackgasse.

Hier ist, warum die anderen arbeiten:

  • ( test1,, test2und test3): Continuations_GetSomeDataAsyncPlant die Fortsetzung des Thread-Pools außerhalb des ASP.NET-Anforderungskontexts. Dadurch kann die TaskRückgabe von Continuations_GetSomeDataAsyncabgeschlossen werden, ohne dass der Anforderungskontext erneut eingegeben werden muss.
  • ( test4Und test6): Da das Taskwird abgewartet , der ASP.NET Anforderungsthread nicht blockiert ist . Auf diese Weise können Sie AsyncAwait_GetSomeDataAsyncden ASP.NET-Anforderungskontext verwenden, wenn Sie fortfahren möchten.

Und hier sind die Best Practices:

  1. asyncVerwenden Sie in Ihren "Bibliotheks" -Methoden, ConfigureAwait(false)wann immer dies möglich ist. In Ihrem Fall würde dies ändern AsyncAwait_GetSomeDataAsyncseinvar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Blockiere nicht auf Tasks; es ist den asyncganzen Weg nach unten. Mit anderen Worten, verwenden Sie awaitanstelle von GetResult( Task.Resultund Task.Waitsollten auch durch ersetzt werden await).

Auf diese Weise erhalten Sie beide Vorteile: Die Fortsetzung (der Rest der AsyncAwait_GetSomeDataAsyncMethode) wird in einem grundlegenden Thread-Pool-Thread ausgeführt, der nicht in den ASP.NET-Anforderungskontext eingegeben werden muss. und der Controller selbst ist async(der einen Anforderungsthread nicht blockiert).

Mehr Informationen:

Update 2012-07-13: Diese Antwort wurde in einen Blog-Beitrag aufgenommen .

Stephen Cleary
quelle
2
Gibt es eine Dokumentation für ASP.NET SynchroniztaionContext, die erklärt, dass für eine Anforderung nur ein Thread im Kontext vorhanden sein kann? Wenn nicht, denke ich, sollte es geben.
Svick
8
Es ist nirgendwo AFAIK dokumentiert.
Stephen Cleary
10
Danke - tolle Antwort. Der Unterschied im Verhalten zwischen (scheinbar) funktional identischem Code ist frustrierend, macht aber bei Ihrer Erklärung Sinn. Es wäre nützlich, wenn das Framework solche Deadlocks erkennen und irgendwo eine Ausnahme auslösen könnte.
Benjamin Fox
3
Gibt es Situationen, in denen die Verwendung von .ConfigureAwait (false) in einem asp.net-Kontext NICHT empfohlen wird? Es scheint mir, dass es immer verwendet werden sollte und dass es nur in einem UI-Kontext nicht verwendet werden sollte, da Sie mit der UI synchronisieren müssen. Oder verpasse ich den Punkt?
AlexGad
3
ASP.NET SynchronizationContextbietet einige wichtige Funktionen: Es fließt der Anforderungskontext. Dies umfasst alle Arten von Dingen, von der Authentifizierung über Cookies bis hin zur Kultur. In ASP.NET synchronisieren Sie also nicht wieder mit der Benutzeroberfläche, sondern wieder mit dem Anforderungskontext. Dies kann sich in Kürze ändern: Das Neue ApiControllerhat einen HttpRequestMessageKontext als Eigenschaft - daher muss der Kontext möglicherweise nicht durchlaufen werden SynchronizationContext-, aber ich weiß es noch nicht.
Stephen Cleary
61

Bearbeiten: Versuchen Sie im Allgemeinen, das Folgende 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;

Aus 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 werden nicht zum aufrufenden Thread zurückgedrängt.

Für mich scheint dies eine brauchbare Option zu sein, da ich nicht die Möglichkeit habe, sie vollständig asynchron zu machen (was ich bevorzugen würde).

Aus der Quelle:

Stellen Sie sicher, dass das Warten in der FooAsync-Methode keinen Kontext findet, in den Sie zurückmarschieren können. Der einfachste Weg, dies zu tun, besteht darin, die asynchrone Arbeit vom ThreadPool aus aufzurufen, z. B. indem Sie den Aufruf in einen Task.Run einschließen, z

int Sync () {return Task.Run (() => Library.FooAsync ()). Ergebnis; }}

FooAsync wird jetzt auf dem ThreadPool aufgerufen, wo kein SynchronizationContext vorhanden ist, und die in FooAsync verwendeten Fortsetzungen werden nicht auf den Thread zurückgesetzt, der Sync () aufruft.

Ykok
quelle
7
Vielleicht möchten Sie Ihren Quelllink erneut lesen. Der Autor empfiehlt , dies nicht zu tun. Funktioniert es? Ja, aber nur in dem Sinne, dass Sie Deadlocks vermeiden. Diese Lösung negiert alle Vorteile von asyncCode in ASP.NET und kann tatsächlich Probleme bei der Skalierung verursachen. Übrigens, ConfigureAwait"bricht in keinem Szenario das richtige asynchrone Verhalten"; Es ist genau das, was Sie im Bibliothekscode verwenden sollten.
Stephen Cleary
2
Es ist der gesamte erste Abschnitt, fett gedruckt Avoid Exposing Synchronous Wrappers for Asynchronous Implementations. Der gesamte Rest des Beitrags erklärt einige verschiedene Möglichkeiten, dies zu tun, wenn Sie dies unbedingt benötigen .
Stephen Cleary
1
Der Abschnitt, den ich in der Quelle gefunden habe, wurde hinzugefügt - ich überlasse es zukünftigen Lesern, zu entscheiden. Beachten Sie, dass Sie im Allgemeinen versuchen sollten, dies zu vermeiden und dies nur als letzten Ausweg zu tun (dh wenn Sie asynchronen Code verwenden, über den Sie keine Kontrolle haben).
Ykok
3
Ich mag alle Antworten hier und wie immer ... sie basieren alle auf dem Kontext (Wortspiel beabsichtigt lol). Ich verpacke die Async-Aufrufe von HttpClient mit einer synchronen Version, sodass ich diesen Code nicht ändern kann, um dieser Bibliothek ConfigureAwait hinzuzufügen. Um die Deadlocks in der Produktion zu verhindern, verpacke ich die Async-Aufrufe in einen Task.Run. So wie ich es verstehe, wird dies 1 zusätzlichen Thread pro Anfrage verwenden und den Deadlock vermeiden. Ich gehe davon aus, dass ich die Synchronisierungsmethoden von WebClient verwenden muss, um vollständig kompatibel zu sein. Das ist eine Menge Arbeit, um dies zu rechtfertigen, daher brauche ich einen zwingenden Grund, um nicht an meinem aktuellen Ansatz festzuhalten.
Samneric
1
Am Ende habe ich eine Erweiterungsmethode erstellt, um Async in Sync zu konvertieren. Ich habe hier irgendwo weitergelesen, wie es das .Net-Framework tut: public static TResult RunSync <TResult> (diese Func <Task <TResult>> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
Samneric
10

Da Sie verwenden .Resultoder .Waitoder awaitdies wird das verursacht ein Ende Sackgasse in Ihrem Code.

Sie verwenden können , ConfigureAwait(false)in asyncfür Methoden verhindern Deadlock

so was:

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

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

Hasan Fathi
quelle
2

Diese beiden Schulen schließen nicht wirklich aus.

Hier ist das Szenario, in dem Sie einfach verwenden müssen

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

oder so ähnlich

   AsyncContext.Run(AsyncOperation);

Ich habe eine MVC-Aktion, die sich unter dem Datenbanktransaktionsattribut befindet. Die Idee war (wahrscheinlich), alles, was in der Aktion getan wurde, zurückzusetzen, wenn etwas schief geht. Dies ermöglicht keine Kontextumschaltung, da sonst das Zurücksetzen oder Festschreiben von Transaktionen selbst fehlschlägt.

Die Bibliothek, die ich benötige, ist asynchron, da erwartet wird, dass sie asynchron ausgeführt wird.

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

Ich sage nur jedem sein eigenes.

alex.peter
quelle
Sie schlagen also die erste Option in Ihrer Antwort vor?
Don Cheadle
1

Ich werde dies hier eher der Vollständigkeit halber als der direkten Relevanz für das OP einfügen. Ich verbrachte fast einen Tag damit, eine HttpClientAnfrage zu debuggen , und fragte mich, warum ich nie eine Antwort zurückbekam.

Schließlich stellte ich fest, dass ich awaitden asyncAnruf weiter unten im Anrufstapel vergessen hatte .

Fühlt sich ungefähr so ​​gut an, als würde ein Semikolon fehlen.

Bondolin
quelle
-1

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 Mitglieder sind für den Compiler vorgesehen.

awaitBrauchen Sie wirklich eine Antwort auf diese Frage, wenn man bedenkt, dass die Version funktioniert und die "richtige" Vorgehensweise ist?

Meine Stimme lautet: Missbrauch der API .

Yamen
quelle
Ich hatte das nicht bemerkt, obwohl ich eine andere Sprache gesehen habe, die darauf hinweist, dass die Verwendung der GetResult () - API ein unterstützter (und erwarteter) Anwendungsfall ist.
Benjamin Fox
1
Darüber hinaus, wenn Sie Test5Controller.Get()den Kellner umgestalten , um Folgendes zu eliminieren: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;Das gleiche Verhalten kann beobachtet werden.
Benjamin Fox