以下は、私が問題を抱えているコードの簡略版です。これをコンソールアプリケーションで実行すると、期待どおりに機能します。すべてのクエリは並行して実行され、すべてが完了するとTask.WaitAll()
が返されます。
ただし、このコードをWebアプリケーションで実行すると、リクエストはハングします。デバッガーを接続してすべてを壊すと、実行がTask.WaitAll()
で待機していることが示されます。そして、最初のタスクは完了しましたが、他のタスクは決して完了しません。
ASP.NETで実行するとハングする理由がわかりませんが、コンソールアプリケーションでは正常に機能します。
public Foo[] DoWork(int[] values)
{
int count = values.Length;
Task[] tasks = new Task[count];
for (int i = 0; i < count; i++)
{
tasks[i] = GetFooAsync(values[i]);
}
try
{
Task.WaitAll(tasks);
}
catch (AggregateException)
{
// Handle exceptions
}
return ...
}
public async Task<Foo> GetFooAsync(int value)
{
Foo foo = null;
Func<Foo, Task> executeCommand = async (command) =>
{
foo = new Foo();
using (SqlDataReader reader = await command.ExecuteReaderAsync())
{
ReadFoo(reader, foo);
}
};
await QueryAsync(executeCommand, value);
return foo;
}
public async Task QueryAsync(Func<SqlCommand, Task> executeCommand, int value)
{
using (SqlConnection connection = new SqlConnection(...))
{
connection.Open();
using (SqlCommand command = connection.CreateCommand())
{
// Set up query...
await executeCommand(command);
// Log results...
return;
}
}
}
Task.WaitAll
ではなく、await Task.WhenAll
を使用する必要があります。
ASP.NETには、実際の同期コンテキストがあります。これは、すべてのawait
呼び出しの後で、継続を実行するためにそのコンテキストにマーシャリングされることを意味します(これらの継続を効果的にシリアル化します)。コンソールアプリには同期コンテキストがないため、すべての継続はスレッドプールに送信されるだけです。リクエストのコンテキストでTask.WaitAll
を使用すると、リクエストがブロックされ、他のすべてのタスクからの継続を処理するために使用されなくなります。
また、ASPアプリでの非同期/待機の主な利点の1つは、リクエストの処理に使用しているスレッドプールスレッドをブロックすることですnotです。 Task.WaitAll
を使用すると、その目的が無効になります。
この変更を行うことの副作用は、ブロッキング操作から待機操作に移行することにより、例外が異なる方法で伝播されることです。 AggregateException
をスローするのではなく、根本的な例外の1つをスローします。