wake-up-neo.com

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

Edit: Diese Frage sieht aus wie es das gleiche Problem sein könnte, hat aber keine Antworten ...

Bearbeiten: Im Testfall 5 scheint die Aufgabe im Zustand WaitingForActivation hängen zu bleiben.

Ich habe mit dem System.Net.Http.HttpClient in .NET 4.5 ein merkwürdiges Verhalten festgestellt, bei dem das "Warten" auf das Ergebnis eines Aufrufs von (z. B.) httpClient.GetAsync(...) niemals zurückgegeben wird.

Dies tritt nur unter bestimmten Umständen auf, wenn die neue Async/Warten-Sprachfunktionalität und die Aufgaben-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 die gleichen Daten zurück (die Antwortheader von stackoverflow.com), mit Ausnahme von /api/test5 was nie abgeschlossen wird.

Habe ich einen Fehler in der HttpClient-Klasse festgestellt oder nutze 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();
    }
}
292
Benjamin Fox

Sie missbrauchen die API.

Dies ist die Situation: In ASP.NET kann jeweils nur ein Thread eine Anfrage bearbeiten. Sie können bei Bedarf eine Parallelverarbeitung durchführen (zusätzliche Threads aus dem Thread-Pool ausleihen), aber nur ein Thread verfügt über den Anforderungskontext (die zusätzlichen Threads verfügen nicht über den Anforderungskontext).

Dies ist verwaltet von ASP.NET SynchronizationContext .

Wenn Sie await ein Task, wird die Methode standardmäßig für ein erfasstes SynchronizationContext (oder ein erfasstes TaskScheduler fortgesetzt, wenn es kein SynchronizationContext gibt ] _). Normalerweise ist dies genau das, was Sie möchten: Eine asynchrone Controller-Aktion wird await ausgeführt, und wenn sie fortgesetzt wird, wird sie mit dem Anforderungskontext fortgesetzt.

Hier ist der Grund, warum test5 Fehlschlägt:

  • Test5Controller.Get Führt AsyncAwait_GetSomeDataAsync Aus (im ASP.NET-Anforderungskontext).
  • AsyncAwait_GetSomeDataAsync Führt HttpClient.GetAsync Aus (im ASP.NET-Anforderungskontext).
  • Die HTTP-Anfrage wird gesendet und HttpClient.GetAsync Gibt ein unvollständiges Task zurück.
  • AsyncAwait_GetSomeDataAsync Erwartet das Task; da es nicht vollständig ist, gibt AsyncAwait_GetSomeDataAsync ein unvollständiges Task zurück.
  • Test5Controller.Get blockiert den aktuellen Thread, bis Task abgeschlossen ist.
  • Die HTTP-Antwort geht ein und das von HttpClient.GetAsync Zurückgegebene Task ist abgeschlossen.
  • AsyncAwait_GetSomeDataAsync Versucht, im ASP.NET-Anforderungskontext fortzufahren. In diesem Kontext ist jedoch bereits ein Thread vorhanden: Der Thread wurde in Test5Controller.Get Blockiert.
  • Sackgasse.

Hier ist, warum die anderen funktionieren:

  • (test1, test2 Und test3): Continuations_GetSomeDataAsync Plant die Fortsetzung des Thread-Pools, außerhalb des ASP .NET-Anforderungskontext. Auf diese Weise kann der von Continuations_GetSomeDataAsync Zurückgegebene Task abgeschlossen werden, ohne dass der Anforderungskontext erneut eingegeben werden muss.
  • (test4 Und test6): Da Task erwartet ist, wird der ASP.NET-Anforderungsthread nicht blockiert. Auf diese Weise kann AsyncAwait_GetSomeDataAsync Den ASP.NET-Anforderungskontext verwenden, wenn er zum Fortfahren bereit ist.

Und hier sind die Best Practices:

  1. Verwenden Sie in Ihren "Bibliothek" async -Methoden ConfigureAwait(false), wann immer dies möglich ist. In Ihrem Fall würde dies AsyncAwait_GetSomeDataAsync In var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); ändern.
  2. Blockiere nicht Tasks; es ist async ganz unten. Mit anderen Worten, verwenden Sie await anstelle von GetResult (Task.Result Und Task.Wait Sollten auch durch await ersetzt werden).

Auf diese Weise erhalten Sie beide Vorteile: Die Fortsetzung (der Rest der AsyncAwait_GetSomeDataAsync - Methode) wird auf einem Basisthreadpool-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 13.07.2012: Diese Antwort wurde eingefügt in einen Blog-Beitrag .

441
Stephen Cleary

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.

Schnellkorrektur 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 es keinen SynchronizationContext gibt, und die in AsyncOperation verwendeten Fortsetzungen werden nicht zum aufrufenden Thread zurückgeführt.

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 vorziehen würde).

Aus der Quelle:

Stellen Sie sicher, dass das Warten in der FooAsync-Methode keinen Kontext findet, in den ein Marshall zurückgeführt werden kann. Der einfachste Weg, dies zu tun, besteht darin, die asynchrone Arbeit aus dem ThreadPool aufzurufen, beispielsweise indem der Aufruf in einen Task.Run, z.

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

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

57
Ykok

Da Sie .Result Oder .Wait Oder await verwenden, führt dies zu einem Deadlock in Ihrem Code.

sie können ConfigureAwait(false) in async -Methoden verwenden, um Deadlock zu verhindern

so was:

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

sie können ConfigureAwait(false) verwenden, wenn dies für Don't Block Async Code möglich ist.

6
Hasan Fathi

Diese beiden Schulen schließen das 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 Attribut "Datenbanktransaktion" befindet. Die Idee war (wahrscheinlich), alles, was in der Aktion gemacht wurde, zurückzusetzen, falls etwas schief geht. Dies erlaubt keine Kontextumschaltung, da andernfalls ein Transaktions-Rollback oder eine Festschreibung von selbst fehlschlägt.

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

Die einzige Möglichkeit. Führen Sie es als normalen Synchronisierungsaufruf aus.

Ich sage nur zu jedem sein eigenes.

1
alex.peter

Ich werde dies hier eher der Vollständigkeit halber als der direkten Relevanz für das OP hinzufügen. Ich habe fast einen Tag damit verbracht, eine HttpClient - Anfrage zu debuggen, und mich gefragt, warum ich nie eine Antwort erhalten habe.

Schließlich stellte ich fest, dass ich vergessen hatte, await den async -Aufruf weiter unten in der Aufrufliste auszuführen.

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

1
Bondolin

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 zu sehen:

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

Wenn man bedenkt, dass die await -Version funktioniert und die 'richtige' Art ist, Dinge zu tun, braucht man wirklich eine Antwort auf diese Frage?

Meine Stimme ist: Missbrauch der API .

0
yamen