1. ホーム
  2. c#

[解決済み] await/asyncを使用しているときにHttpClient.GetAsync(...)が返らない

2022-03-16 05:21:47

質問

編集してください。 この質問 同じ問題のように見えますが、回答がありません...。

編集する テストケース5では、タスクが WaitingForActivation の状態です。

.NET 4.5 の System.Net.Http.HttpClient を使用して、奇妙な動作に遭遇しました - (例) への呼び出しの結果を "await" する場合。 httpClient.GetAsync(...) は決して戻りません。

これは、新しい async/await 言語機能と Tasks API を使用した場合にのみ発生する現象で、継続のみを使用した場合は常にコードが動作するようです。

Visual Studio 11 の新しい "MVC 4 WebApi project" にこのコードをドロップして、次の GET エンドポイントを公開します。

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

ここでの各エンドポイントは、同じデータ (stackoverflow.com からのレスポンスヘッダ) を返します。 /api/test5 は決して完了しません。

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

解決方法は?

APIを誤って使用している。

ASP.NETでは、一度に1つのスレッドしかリクエストを処理できないのです。必要であれば並列処理を行うことができますが (スレッドプールから追加のスレッドを借りる)、 リクエストコンテキストを持つスレッドはひとつだけです (追加のスレッドはリクエストコンテキストを持ちません)。

これは ASP.NETで管理される SynchronizationContext .

デフォルトでは await a Task をキャプチャすると、メソッドが再開されます。 SynchronizationContext (またはキャプチャされた TaskScheduler がない場合は SynchronizationContext ). 通常、これはちょうどあなたが望むことです。非同期コントローラアクションが await を実行し、再開するときにはリクエストのコンテキストで再開します。

では、なぜ 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_GetSomeDataAsync はASP.NETリクエストコンテキスト内で再開を試みます。しかし、そのコンテキストにはすでにスレッドが存在します。 Test5Controller.Get .
  • デッドロック

他のものはなぜ動くのか、その理由を説明します。

  • ( test1 , test2 および test3 ): Continuations_GetSomeDataAsync は、スレッドプールに継続をスケジュールします。 外側 ASP.NETのリクエストコンテキストを使用します。これにより Task が返す Continuations_GetSomeDataAsync を使えば、 リクエストコンテキストを再入力することなく完了します。
  • ( test4test6 ): というのは Task 待機中 の場合、ASP.NETリクエストスレッドはブロックされません。このため AsyncAwait_GetSomeDataAsync を使用して、継続の準備ができたときにASP.NETリクエストコンテキストを使用することができます。

そして、ベストプラクティスはこちらです。

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

そうすることで、継続(の残り)という両方のメリットを得ることができます。 AsyncAwait_GetSomeDataAsync メソッド) は ASP.NET リクエストコンテキストに入る必要のない基本的なスレッドプールスレッドで実行されます。 async (これはリクエストスレッドをブロックしません)。

詳細はこちら

2012-07-13に更新しました。 この回答を組み入れました ブログ記事へ .