await / asyncを使用すると、HttpClient.GetAsync(…)が返されない


315

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

編集:テストケース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();
    }
}

2
参照-それは同じ問題ではありませんが、ちょうどあなたがそれについて知っている作るために、同期の完全なことをベータWRT非同期メソッドでMVC4のバグがありますstackoverflow.com/questions/9627329/...
ジェームズ・マニング

ありがとう-気をつけましょう。この場合、HttpClient.GetAsync(...)?への呼び出しのため、メソッドは常に非同期である必要があると思います。
ベンジャミンフォックス

回答:


468

APIを誤用しています。

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

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

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

だから、ここで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):Continuations_GetSomeDataAsyncスケジュールスレッドプールに続き、外部 ASP.NET要求コンテキスト。これにより、リクエストコンテキストを再入力することなくTask、によって返されたContinuations_GetSomeDataAsyncを完了できます。
  • test4およびtest6):Task待機しているため、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.Waitもに置き換えなければなりませんawait)。

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

詳しくは:

アップデート2012-07-13:この回答をブログ投稿に組み込みました


2
SynchroniztaionContext一部のリクエストのコンテキストにはスレッドが1つしかないことを説明するASP.NETのドキュメントはありますか?そうでなければ、あるべきだと思います。
12

8
AFAIKのどこにも文書化されていません。
Stephen Cleary

10
ありがとう- 素晴らしい応答。(見たところ)機能的に同一のコード間の動作の違いはイライラさせられますが、あなたの説明では理にかなっています。フレームワークがそのようなデッドロックを検出し、どこかで例外を発生させることができれば有用でしょう。
ベンジャミンフォックス

3
asp.netコンテキストで.ConfigureAwait(false)を使用することが推奨されない状況はありますか?それは常に使用されるべきであり、UIに同期する必要があるため、使用されるべきではないUIコンテキストでのみであるように私には思えます。または私はポイントを逃していますか?
AlexGad

3
ASP.NET SynchronizationContextはいくつかの重要な機能を提供します。それは要求コンテキストをフローします。これには、認証からCookie、カルチャーまで、あらゆる種類のものが含まれます。したがって、ASP.NETでは、UIに同期する代わりに、要求コンテキストに同期します。これは間もなく変更される可能性があります。新しいApiControllerものにはHttpRequestMessageコンテキストがプロパティとして含まれているため、コンテキストをフローする必要ない場合もありますが、SynchronizationContextまだわかりません。
スティーブンクリアリー

62

編集:一般に、デッドロックを回避するための最後の溝を除いて、以下を実行しないようにします。スティーブン・クリアリーからの最初のコメントを読んでください。

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

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

試してください:

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

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

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

ソースから(上記の例と一致するように編集):

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

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

ソースから:

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

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

これで、FooAsyncはThreadPoolで呼び出されます。SynchronizationContextはなく、FooAsync内で使用される継続は、Sync()を呼び出しているスレッドに強制的に戻されません。


7
ソースリンクをもう一度読みたいかもしれません。著者はこれを行わないことを推奨しいます。うまくいきますか?はい。ただし、デッドロックを回避するという意味でのみです。このソリューションは、ASP.NET のコードのすべての利点を無効にasync実際に大規模な問題を引き起こす可能性があります。ところで、ConfigureAwaitどのシナリオでも「適切な非同期動作を壊す」ことはありません。それはまさにあなたがライブラリコードで使用すべきものです。
Stephen Cleary 2013年

2
太字のタイトルが付いた最初のセクション全体Avoid Exposing Synchronous Wrappers for Asynchronous Implementationsです。投稿の残り全体では、どうしても必要な場合にそれを行うためのいくつかの異なる方法を説明しています。
Stephen Cleary 2013年

1
ソースで見つけたセクションを追加しました-決定するのは将来の読者に任せます。通常、これを回避することを試み、最後の手段としてのみ実行する必要があることに注意してください(つまり、制御できない非同期コードを使用する場合)。
Ykok、2013

3
私はここですべての答えが好きで、いつものように....それらはすべてコンテキストに基づいています(しゃれた意図的な笑)。HttpClientの非同期呼び出しを同期バージョンでラップしているので、そのコードを変更してConfigureAwaitをそのライブラリに追加することはできません。本番環境でのデッドロックを防ぐために、非同期呼び出しをTask.Runでラップしています。私が理解しているように、これはリクエストごとに1つの追加スレッドを使用し、デッドロックを回避します。完全に準拠するには、WebClientの同期メソッドを使用する必要があると思います。それは正当化する多くの仕事なので、私の現在のアプローチに固執しない説得力のある理由が必要になります。
samneric 2014年

1
結局、AsyncをSyncに変換する拡張メソッドを作成しました。.Netフレームワークがそれを行うのと同じ方法でここで読みます:public static TResult RunSync <TResult>(this Func <Task <TResult >> func){return _taskFactory .StartNew(func).Unwrap().GetAwaiter() .GetResult(); }
samneric

10

あなたが使用しているので.Result.Waitまたはawaitこれが原因となってしまいますデッドロックをあなたのコードに。

あなたが使用することができますConfigureAwait(false)asyncする方法デッドロックを防止します

このような:

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

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


2

これらの2つの学校は本当に除外されていません。

これは単に使用する必要があるシナリオです

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

または何かのような

   AsyncContext.Run(AsyncOperation);

データベーストランザクション属性の下にあるMVCアクションがあります。アイデアは、(おそらく)何かがうまくいかなかった場合にアクションで行われたすべてをロールバックすることでした。これはコンテキストの切り替えを許可しません。そうしないと、トランザクションのロールバックまたはコミット自体が失敗します。

私が必要とするライブラリは非同期で実行することが期待されているため、非同期です。

唯一のオプションです。通常の同期呼び出しとして実行します。

私はそれぞれに独自に言っているだけです。


あなたはあなたの答えの最初の選択肢を提案していますか?
Don Cheadle

1

ここでは、完全性のために、OPに直接関連するものではなく、これをここに配置します。HttpClientリクエストのデバッグに1日近く費やしましたが、なぜ応答が返されなかったのか疑問に思いました。

最後に、私は忘れていたことがわかったコールスタックダウン、さらにコール。awaitasync

セミコロンがないのと同じくらいいい感じです。


-1

私はここを探しています:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx

そしてここ:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

そして見て:

この型とそのメンバーは、コンパイラーによる使用を目的としています。

awaitバージョンが機能し、「正しい」方法であることを考えると、この質問に対する答えが本当に必要ですか?

私の投票は:APIの誤用です。


GetResult()APIの使用がサポートされている(そして期待されている)ユースケースであることを示す他の言語を見たことはありますが、私はそれに気づいていませんでした。
ベンジャミンフォックス

1
さらに、次のようにリファクタリングTest5Controller.Get()してウェイターを排除すると、var task = AsyncAwait_GetSomeDataAsync(); return task.Result;同じ動作が見られます。
Benjamin Fox
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.