web-dev-qa-db-ja.com

await / asyncを使用するとHttpClient.GetAsync(...)が返されることはありません

編集:この質問 は同じ問題かもしれませんが、応答がありません...

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

APIを誤用しています。

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

これは ASP.NET SynchronizationContextで管理 です。

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

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

  • Test5Controller.GetAsyncAwait_GetSomeDataAsyncを実行します(ASP.NET要求コンテキスト内)。
  • AsyncAwait_GetSomeDataAsyncHttpClient.GetAsyncを実行します(ASP.NET要求コンテキスト内)。
  • HTTP要求が送信され、HttpClient.GetAsyncは未完了のTaskを返します。
  • AsyncAwait_GetSomeDataAsyncTaskを待ちます;完全ではないため、AsyncAwait_GetSomeDataAsyncは未完了のTaskを返します。
  • Test5Controller.GetblocksTaskが完了するまで現在のスレッドをブロックします。
  • HTTP応答が受信され、HttpClient.GetAsyncによって返されたTaskが完了します。
  • AsyncAwait_GetSomeDataAsyncは、ASP.NET要求コンテキスト内で再開を試みます。ただし、そのコンテキストにはすでにスレッドがあります:Test5Controller.Getでブロックされているスレッド。
  • デッドロック。

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

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

そして、ここにベストプラクティスがあります。

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

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

詳しくは:

2012-07-13の更新:この回答を組み込みました ブログ投稿へ

441
Stephen Cleary

編集:デッドロックを回避するための最後の努力として以外は、一般的に以下のことを避けるようにしてください。 Stephen Clearyからの最初のコメントを読んでください。

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

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

試してください:

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

あるいは結果が必要な場合は

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

ソースから(上の例に合うように編集された):

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

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

ソースから:

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

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

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

56
Ykok

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

デッドロックを防ぐためにasyncメソッドでConfigureAwait(false)を使うことができます

このような:

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

「非同期コードをブロックしない」では、可能な限りConfigureAwait(false)を使用できます。

4
Hasan Fathi

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

これはあなたが単に使用しなければならないシナリオです。

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

または何かのような

   AsyncContext.Run(AsyncOperation);

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

私が必要とするライブラリはasyncを実行することが期待されているのでasyncです。

唯一の選択肢です。通常の同期呼び出しとして実行してください。

私はただお互いに言っているだけです。

1
alex.peter

私はここを見ています。

http://msdn.Microsoft.com/ja-jp/library/system.runtime.compilerservices.taskawaiter(v = vs.110).aspx

そしてここ:

http://msdn.Microsoft.com/ja-jp/library/system.runtime.compilerservices.taskawaiter.getresult(v = vs.110).aspx

そして見て:

この型とそのメンバは、コンパイラで使用するためのものです。

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

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

0
yamen

私はここでOPへの直接の関連性より完全性のためにこれをもっと入れるつもりです。私は1日近くかけてHttpClientリクエストをデバッグしましたが、なぜ応答が返ってこなかったのか疑問に思いました。

最後に、呼び出しスタックのさらに下のawait呼び出しをasyncに忘れていたことがわかりました。

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

0
Bondolin