344

編集: この質問は同じ問題のように見えますが、回答がありません...

編集:テスト ケース 5 では、タスクがスタックWaitingForActivation状態にあるように見えます。

.NET 4.5 で System.Net.Http.HttpClient を使用すると、いくつかの奇妙な動作が発生しました。(eg) への呼び出しの結果を「待機」すると、httpClient.GetAsync(...)返されることはありません。

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

問題を再現するコードを次に示します。これを Visual Studio 11 の新しい「MVC 4 WebApi プロジェクト」にドロップして、次の 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();
    }
}
4

7 に答える 7

499

API を悪用しています。

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

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

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

したがって、test5失敗する理由は次のとおりです。

  • Test5Controller.GetAsyncAwait_GetSomeDataAsync(ASP.NET 要求コンテキスト内で)を実行します。
  • AsyncAwait_GetSomeDataAsyncHttpClient.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ます。
  • デッドロック。

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

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

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

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

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

詳しくは:

2012 年 7 月 13 日更新:この回答をブログ投稿に組み込みました

于 2012-04-27T13:20:20.143 に答える
65

編集:通常、デッドロックを回避するための最後の努力を除いて、以下を実行しないようにしてください。Stephen Cleary からの最初のコメントを読んでください。

ここからクイックフィックス。書く代わりに:

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

試す:

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

または、結果が必要な場合:

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

ソースから (上記の例に合わせて編集):

AsyncOperation は、SynchronizationContext が存在しない ThreadPool で呼び出されるようになり、AsyncOperation 内で使用される継続は、呼び出し元のスレッドに強制的に戻されません。

私にとっては、これは使用可能なオプションのように見えます。これは、完全に非同期にするオプションがないためです (私が好む)。

ソースから:

FooAsync メソッドの await でマーシャリング先のコンテキストが見つからないことを確認してください。これを行う最も簡単な方法は、呼び出しを Task.Run でラップするなどして、ThreadPool から非同期作業を呼び出すことです。

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

FooAsync は、SynchronizationContext が存在しない ThreadPool で呼び出されるようになり、FooAsync の内部で使用される継続は、Sync() を呼び出しているスレッドに強制的に戻されません。

于 2013-11-01T13:23:52.080 に答える
16

.Resultor .Waitorを使用awaitしているため、コードでデッドロックが発生します。

デッドロックを防ぐためConfigureAwait(false)asyncメソッドで使用できます

このような:

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

ConfigureAwait(false)可能な限り Don't Block Async Code を使用できます。

于 2018-01-20T15:12:52.873 に答える
1

OPとの直接的な関連性よりも完全性のために、これをここに入れます。リクエストのデバッグにほぼ 1 日を費やし、HttpClientなぜレスポンスが返ってこないのか疑問に思いました。

最後に、呼び出しスタックのさらに下にある呼び出しをawait忘れていたことに気付きました。async

セミコロンが欠けているのと同じくらい良い感じです。

于 2019-05-21T19:05:27.137 に答える