HttpClient.GetAsync(...) は await/async を使用すると返されません 質問する

HttpClient.GetAsync(...) は await/async を使用すると返されません 質問する

編集: この質問同じ問題のようですが、応答がありません...

編集:テスト ケース 5 では、タスクがWaitingForActivation状態のままになっているようです。

.NET 4.5 で System.Net.Http.HttpClient を使用しているときに、奇妙な動作が発生しました。たとえば、呼び出しの結果を「待機」しても、結果httpClient.GetAsync(...)が返されません。

これは、新しい async/await 言語機能と Tasks API を使用する特定の状況でのみ発生します。継続のみを使用すると、コードは常に機能するようです。

問題を再現するコードを以下に示します。これを Visual Studio 11 の新しい「MVC 4 WebApi プロジェクト」にドロップして、次の GET エンドポイントを公開します。

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

ここでの各エンドポイントは、/api/test5完了しないものを除いて同じデータ (stackoverflow.com からの応答ヘッダー) を返します。

HttpClient クラスでバグが発生したのでしょうか、それとも API を何らかの方法で誤用しているのでしょうか?

再現するコード:

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

ベストアンサー1

API を誤用しています。

状況は次のとおりです。ASP.NET では、一度に 1 つのスレッドのみが要求を処理できます。必要に応じて並列処理 (スレッド プールから追加のスレッドを借りる) を実行できますが、要求コンテキストを持つスレッドは 1 つだけです (追加のスレッドには要求コンテキストはありません)。

これはASP.NETによって管理されるSynchronizationContext

デフォルトでは、awaitを実行するとTask、メソッドはキャプチャされた(がない場合はSynchronizationContextキャプチャされた)で再開します。通常、これはまさに必要な動作です。非同期コントローラー アクションが何かを実行し、再開すると、リクエスト コンテキストで再開します。TaskSchedulerSynchronizationContextawait

では、test5失敗する理由は次のとおりです。

  • Test5Controller.Get実行されますAsyncAwait_GetSomeDataAsync(ASP.NET 要求コンテキスト内)。
  • AsyncAwait_GetSomeDataAsync実行されますHttpClient.GetAsync(ASP.NET 要求コンテキスト内)。
  • HTTP リクエストが送信され、HttpClient.GetAsync未完了の が返されますTask
  • AsyncAwait_GetSomeDataAsyncを待機しますTask。完了していないため、AsyncAwait_GetSomeDataAsync未完了の を返しますTask
  • Test5Controller.Get それが完了するまで現在のスレッドをブロックしますTask
  • HTTP レスポンスが到着し、Taskによって返される処理HttpClient.GetAsyncが完了します。
  • AsyncAwait_GetSomeDataAsyncASP.NET 要求コンテキスト内で再開しようとします。ただし、そのコンテキストにはすでにスレッドが存在します。スレッドは でブロックされていますTest5Controller.Get
  • 行き詰まり。

他のものが機能する理由は次のとおりです。

  • ( test1、、test2およびtest3): ASP.NET 要求コンテキストの外部でContinuations_GetSomeDataAsync、スレッド プールへの継続をスケジュールします。これにより、 によって返された は、要求コンテキストに再度入ることなく完了できます。TaskContinuations_GetSomeDataAsync
  • (test4およびtest6): が待機されてTaskいるため、ASP.NET 要求スレッドはブロックされません。これにより、続行する準備ができたときに ASP.NET 要求コンテキストを使用できます。AsyncAwait_GetSomeDataAsync

ベストプラクティスは次のとおりです。

  1. 「ライブラリ」asyncメソッドでは、ConfigureAwait(false)可能な限り使用してください。あなたの場合、これはAsyncAwait_GetSomeDataAsync次のように変更されます。var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. sでブロックしないでください。s はずっと下にTaskあります。つまり、 (の代わりにを使用し、も に置き換える必要があります)。asyncawaitGetResultTask.ResultTask.Waitawait

こうすることで、継続 (メソッドの残りの部分AsyncAwait_GetSomeDataAsync) が ASP.NET 要求コンテキストに入る必要のない基本スレッド プール スレッドで実行され、コントローラー自体もasync(要求スレッドをブロックしない) 実行されるという両方の利点が得られます。

詳しくは:

2012-07-13更新:この回答を組み込んだブログ記事に

おすすめ記事