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

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

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

がここにありますデモこの動作の例です。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同じ奇妙な動作が発生します。

もう一つの観察:ラッパーを a 内にラップするとTask.Run継続が非同期になると思っていました。しかし、それは起こりませんでした。以下の行の継続は、同じスレッドで (同期的に) 引き続き実行されます。

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

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

ベストアンサー1

つまり、同じタスク変数を待機する複数の非同期メソッドがあることになります。

    await task;
    // CPU heavy operation

はい、これらの継続は完了時に連続して呼び出されますtask。あなたの例では、各継続は次の 1 秒間スレッドを占有します。

各継続を非同期で実行したい場合は、次のようなものが必要になる場合があります。

    await task;
    await Task.Yield().ConfigureAwait(false);
    // CPU heavy operation

タスクが最初の継続から戻り、CPU 負荷が の外部で実行できるようにしますSynchronizationContext

おすすめ記事