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


14

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

ここでデモこの動作のは。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では、元のタスクの待機とタスクラッパーの待機の間に違いはありません。どちらの場合も、継続は同じスレッドで同期的に実行されます。


好奇心旺盛な場合、どうなりawait Task.WhenAll(new[] { task });ますか?
vasily.sib

1
内部での短絡が原因だと思いますTask.WhenAll
Michael Randall

3
LinqPadは両方のバリアントに対して同じ期待される2番目の出力を提供します...並列実行を取得するために使用する環境(コンソール対WinForms対...、。NET対コア、...、フレームワークバージョン)?
Alexei Levenkov

1
.NET Core 3.0と3.1でこの動作を再現することができましたが、edで完了しないように初期値をTask.Delayから100に変更した後にのみです。1000await
Stephen Cleary

2
@BlueStrat素敵な発見!それは確かに何らかの形で関連している可能性があります。興味深いことに、.NET Frameworks 4.6、4.6.1、4.7.1、4.7.2および4.8 でMicrosoftのコードの誤った動作を再現できませんでした。毎回異なるスレッドIDを取得しますが、これは正しい動作です。ここでは 4.7.2上で実行されているフィドルです。
Theodor Zoulias

回答:


2

したがって、同じタスク変数を待機する複数の非同期メソッドがあります。

    await task;
    // CPU heavy operation

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

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

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

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


答えをくれたジェレミーに感謝します。はい、Task.Yield私の問題に対する良い解決策です。私の質問は、なぜこれが起こっているのかについてより多くのことであり、望ましい振る舞いを強制する方法についてはそれほどではありません。
Theodor Zoulias

あなたが本当に知りたいのであれば、ソースコードはここにあります。github.com/microsoft/referencesource/blob/master/mscorlib/...
ジェレミーLakeman

関連するクラスのソースコードを研究することによって私の質問の答えを得ることが、それ程簡単だったことを願っています。コードを理解し、何が起こっているのかを理解するには、私には年齢がかかります。
Theodor Zoulias

重要なのはを回避することです。元のタスクSynchronizationContextConfigureAwait(false)1回呼び出すだけで十分な場合があります。
Jeremy Lakeman

これはコンソールアプリケーションであり、SynchronizationContext.Currentnullです。しかし、私はそれを確かめるためにチェックしたところです。行に追加ConfigureAwait(false)しましたが、await違いはありませんでした。観察結果は以前と同じです。
Theodor Zoulias

1

を使用してタスクを作成するとTask.Delay()、その作成オプションはではNoneなくに設定されますRunContinuationsAsychronously

これは、.netフレームワークと.netコアの間の重大な変更である可能性があります。それにもかかわらず、あなたが観察している行動を説明しているように見えます。また、ソースコードに掘ってからこれを確認することができますTask.Delay()されるまでNEWINGDelayPromiseデフォルト呼び出すTask指定なし作成オプションを残さないコンストラクタを。


タンビーアに答えてくれてありがとう。新しいオブジェクトを作成するときRunContinuationsAsychronouslyNone、.NET Coreではがの代わりにデフォルトになったと推測しTaskますか?これは私の観察結果の一部ではあるがすべてではない。具体的には、同じTask.WhenAllラッパーを待つことと、異なるラッパーを待つことの違いについては説明しません。
Theodor Zoulias

0

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

var task = Task.Delay(100);

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

await task;

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

await Task.WhenAll(task);

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

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

答えてくれてSeyedraoufに感謝します。あなたの説明は私にとっても満足のいくものではありません。によって返されるタスクは、元のタスクと同様に、Task.WhenAll通常のタスクです。両方のタスクは、ある時点で完了しています。タイマーイベントの結果としての元のタスクと、元のタスクの完了の結果としての複合タスクです。それらの継続が異なる動作を表示するのはなぜですか?1つのタスクが他のタスクと異なる点は何ですか?Tasktask
Theodor Zoulias
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.