web-dev-qa-db-ja.com

Task.WhenAllの継続が同期的に実行されるのはなぜですか?

.NET Core 3.0で実行しているときに、 _Task.WhenAll_ メソッドに関して奇妙な観察をしました。単純な_Task.Delay_タスクを単一の引数として_Task.WhenAll_に渡したところ、ラップされたタスクが元のタスクと同じように動作することを期待していました。しかし、そうではありません。元のタスクの継続は非同期的に実行され(これは望ましい)、複数のTask.WhenAll(task)ラッパーの継続は次々に同期的に実行されます(これは望ましくありません)。

これがこの動作の demo です。 4つのワーカータスクは、同じ_Task.Delay_タスクが完了するのを待ってから、重い計算(_Thread.Sleep_でシミュレート)を続行します。

_var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");

    await task;
    //await Task.WhenAll(task);

    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");

    Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);
_

これが出力です。 4つの継続は、異なるスレッドで(並列に)期待どおりに実行されています。

_05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await
_

ここで、行_await task_をコメント化し、次の行await Task.WhenAll(task)のコメントを外すと、出力はかなり異なります。すべての継続は同じスレッドで実行されるため、計算は並列化されません。各計算は、前の計算の完了後に開始されます。

_05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await
_

驚くべきことに、これは各ワーカーが異なるラッパーを待機している場合にのみ発生します。ラッパーを事前に定義すると、次のようになります。

_var task = Task.WhenAll(Task.Delay(500));
_

...そしてすべてのワーカー内で同じタスクをawaitすると、動作は最初のケースと同じになります(非同期継続)。

私の質問は:なぜこれが起こっているのですか?同じタスクの異なるラッパーの継続が同じスレッドで同期的に実行される原因は何ですか?

注:_Task.WhenAny_の代わりに _Task.WhenAll_ でタスクをラップすると、同じ奇妙な動作になります。

別の観察:_Task.Run_内にラッパーをラップすると、継続が非同期になると予想していました。しかし、それは起こっていません。以下の行の続きは、引き続き同じスレッドで(同期的に)実行されます。

_await Task.Run(async () => await Task.WhenAll(task));
_

明確化:上記の違いは、.NET Core 3.0プラットフォームで実行されているコンソールアプリケーションで観察されました。 .NET Framework 4.8では、元のタスクの待機とタスクラッパーの待機の間に違いはありません。どちらの場合も、継続は同じスレッドで同期的に実行されます。

14
Theodor Zoulias

あなたのコードでは、次のコードは繰り返しの本体の外にあります。

var task = Task.Delay(100);

したがって、以下を実行するたびに、タスクを待機して別のスレッドで実行します

await task;

しかし、以下を実行すると、taskの状態がチェックされるため、1つのスレッドで実行されます

await Task.WhenAll(task);

しかし、タスクの作成をWhenAllの横に移動すると、各タスクが個別のスレッドで実行されます。

var task = Task.Delay(100);
await Task.WhenAll(task);