編集: この質問同じ問題のようですが、応答がありません...
編集:テスト ケース 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
キャプチャされた)で再開します。通常、これはまさに必要な動作です。非同期コントローラー アクションが何かを実行し、再開すると、リクエスト コンテキストで再開します。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
): ASP.NET 要求コンテキストの外部でContinuations_GetSomeDataAsync
、スレッド プールへの継続をスケジュールします。これにより、 によって返された は、要求コンテキストに再度入ることなく完了できます。Task
Continuations_GetSomeDataAsync
- (
test4
およびtest6
): が待機されてTask
いるため、ASP.NET 要求スレッドはブロックされません。これにより、続行する準備ができたときに ASP.NET 要求コンテキストを使用できます。AsyncAwait_GetSomeDataAsync
ベストプラクティスは次のとおりです。
- 「ライブラリ」
async
メソッドでは、ConfigureAwait(false)
可能な限り使用してください。あなたの場合、これはAsyncAwait_GetSomeDataAsync
次のように変更されます。var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
- sでブロックしないでください。s はずっと下に
Task
あります。つまり、 (の代わりにを使用し、も に置き換える必要があります)。async
await
GetResult
Task.Result
Task.Wait
await
こうすることで、継続 (メソッドの残りの部分AsyncAwait_GetSomeDataAsync
) が ASP.NET 要求コンテキストに入る必要のない基本スレッド プール スレッドで実行され、コントローラー自体もasync
(要求スレッドをブロックしない) 実行されるという両方の利点が得られます。
詳しくは:
- 私の
async
/await
紹介記事Task
これには、待機者が を使用する方法についての簡単な説明が含まれていますSynchronizationContext
。 - のAsync/Await に関するよくある質問では、文脈についてより詳しく説明しています。また、Await、UI、そしてデッドロック! ああ、大変!これは、UI ではなく ASP.NET を使用している場合でも当てはまります
SynchronizationContext
。ASP.NET では、要求コンテキストが一度に 1 つのスレッドに制限されるためです。 - これMSDNフォーラム投稿。
- スティーブン・トーブこのデッドロックをデモします(UI を使用)、 そしてルシアン・ウィシックも同様だ。
2012-07-13更新:この回答を組み込んだブログ記事に。