378 Stimmen

HttpClient.GetAsync(...) gibt bei Verwendung von await/async nie zurück

Edita: Diese Frage sieht so aus, als ob es sich um dasselbe Problem handeln könnte, hat aber keine Antworten...

Edita: Im Testfall 5 scheint die Aufgabe in der folgenden Situation stecken zu bleiben WaitingForActivation Zustand.

Ich habe einige seltsame Verhalten mit dem System.Net.Http.HttpClient in .NET 4.5 - wo "warten" das Ergebnis eines Aufrufs an (z. B.) gefunden. httpClient.GetAsync(...) wird niemals zurückkehren.

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

Hier ist etwas Code, der das Problem reproduziert - legen Sie diese in ein neues "MVC 4 WebApi-Projekt" in Visual Studio 11, um die folgenden GET-Endpunkte freizugeben:

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

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

Bin ich auf einen Fehler in der HttpClient-Klasse gestoßen, oder verwende ich die API auf irgendeine Weise falsch?

Code zum Reproduzieren:

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();
    }
}

527voto

Stephen Cleary Punkte 402664

Sie missbrauchen die API.

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

Dies ist verwaltet von der ASP.NET SynchronizationContext .

Standardmäßig, wenn Sie await a Task wird die Methode mit einem gefangenen SynchronizationContext (oder eine erbeutete TaskScheduler wenn es keine SynchronizationContext ). Normalerweise ist dies genau das, was Sie wollen: eine asynchrone Controller-Aktion wird await etwas, und wenn es weitergeht, geht es mit dem Anfragekontext weiter.

Und das ist der Grund test5 scheitert:

  • Test5Controller.Get führt aus. AsyncAwait_GetSomeDataAsync (innerhalb des ASP.NET-Anfragekontexts).
  • AsyncAwait_GetSomeDataAsync führt aus. HttpClient.GetAsync (innerhalb des ASP.NET-Anfragekontexts).
  • Die HTTP-Anfrage wird gesendet, und HttpClient.GetAsync gibt ein unvollständiges Task .
  • AsyncAwait_GetSomeDataAsync erwartet die Task ; da sie nicht vollständig ist, AsyncAwait_GetSomeDataAsync gibt ein unvollständiges Task .
  • Test5Controller.Get Blöcke den aktuellen Thread, bis dieser Task komplettiert.
  • Die HTTP-Antwort kommt an, und die Task zurückgegeben von HttpClient.GetAsync abgeschlossen ist.
  • AsyncAwait_GetSomeDataAsync versucht, innerhalb des ASP.NET-Anforderungskontexts fortzufahren. Es gibt jedoch bereits einen Thread in diesem Kontext: der Thread, der in Test5Controller.Get .
  • Sackgasse.

Hier ist, warum die anderen funktionieren:

  • ( test1 , test2 y test3 ): Continuations_GetSomeDataAsync plant die Fortsetzung in den Threadpool ein, außerhalb den ASP.NET-Anfragekontext. Dies ermöglicht die Task zurückgegeben von Continuations_GetSomeDataAsync abzuschließen, ohne den Anfragekontext erneut eingeben zu müssen.
  • ( test4 y test6 ): Da die Task es erwartet wird der ASP.NET-Anfrage-Thread nicht blockiert. Dies ermöglicht AsyncAwait_GetSomeDataAsync um den ASP.NET-Anforderungskontext zu verwenden, wenn er bereit ist, fortzufahren.

Und hier sind die besten Praktiken:

  1. In Ihrer "Bibliothek" async Methoden, Verwendung ConfigureAwait(false) wann immer möglich. In Ihrem Fall würde dies bedeuten AsyncAwait_GetSomeDataAsync zu sein var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. Nicht blockieren auf Task s; es ist async den ganzen Weg nach unten. Mit anderen Worten, verwenden Sie await anstelle von GetResult ( Task.Result y Task.Wait sollte ebenfalls ersetzt werden durch await ).

Auf diese Weise erhalten Sie beide Vorteile: die Fortsetzung (der Rest des AsyncAwait_GetSomeDataAsync Methode) wird auf einem einfachen Thread-Pool-Thread ausgeführt, der nicht in den ASP.NET-Anfragekontext eintreten muss; und der Controller selbst ist async (wodurch ein Anfrage-Thread nicht blockiert wird).

Weitere Informationen:

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

79voto

Ykok Punkte 1105

Bearbeiten: Versuchen Sie generell, die folgenden Schritte zu vermeiden, es sei denn, es handelt sich um einen letzten Versuch, um Deadlocks zu vermeiden. Lesen Sie den ersten Kommentar von Stephen Cleary.

Schnelle Lösung von aquí . Anstatt zu schreiben:

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

Versuchen Sie es:

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

Oder wenn Sie ein Ergebnis brauchen:

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

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

Die AsyncOperation wird nun auf dem ThreadPool aufgerufen, wo es kein SynchronizationContext vorhanden ist, und die Fortsetzungen, die innerhalb des von AsyncOperation verwendeten Fortsetzungen werden nicht zum aufrufenden Thread zurückgeführt.

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

Von der Quelle:

Stellen Sie sicher, dass das await in der Methode FooAsync zurück zu marschieren. Der einfachste Weg, dies zu tun, ist der Aufruf der asynchrone Arbeit aus dem ThreadPool aufzurufen, z. B. durch Umhüllen des Aufruf in eine Task.Run verpackt, z. B.

i return Task.Run(() => Library.FooAsync()).Result; }

FooAsync wird nun im ThreadPool aufgerufen, wo es keine SynchronizationContext, und die Fortsetzungen, die innerhalb von FooAsync verwendet werden, werden nicht mehr zu dem Thread zurückgeführt, der Sync() aufruft.

24voto

Hasan Fathi Punkte 4840

Da Sie die .Result o .Wait o await wird dies zu einer Deadlock in Ihrem Code.

können Sie ConfigureAwait(false) en async Methoden für Verhinderung einer Blockade

をこのように

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

können Sie ConfigureAwait(false) wo immer möglich für Don't Block Async Code .

4voto

alex.peter Punkte 226

Diese beiden Schulen sind nicht wirklich ausgeschlossen.

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

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

oder etwas wie

   AsyncContext.Run(AsyncOperation);

Ich habe eine MVC-Aktion, die unter Datenbank-Transaktionsattribut ist. Die Idee war (wahrscheinlich), um alles in der Aktion getan zurückzusetzen, wenn etwas schief geht. Dies erlaubt keine Kontextwechsel, sonst Transaktion Rollback oder Commit wird selbst fehlschlagen.

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

Die einzige Möglichkeit. Führen Sie ihn als normalen Sync-Aufruf aus.

Ich sage nur: Jedem das Seine.

3voto

Bondolin Punkte 2504

Ich füge dies hier mehr der Vollständigkeit halber ein, als dass es einen direkten Bezug zum OP hätte. Ich habe fast einen Tag mit der Fehlersuche in einem HttpClient Anfrage und fragte mich, warum ich nie eine Antwort erhielt.

Schließlich stellte ich fest, dass ich vergessen hatte await die async Aufruf weiter unten im Aufrufstapel.

Fühlt sich ungefähr so gut an wie ein fehlendes Semikolon.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X