複数の待機よりも単一の「待機Task.WhenAll」を優先する必要があるのはなぜですか?


126

タスク完了の順序を気にせず、すべて完了する必要がある場合でもawait Task.WhenAll、複数の代わりに使用する必要がありますawaitか?たとえば、DoWork2以下の方法が推奨されますDoWork1(そしてその理由は?):

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}

2
並行して実行する必要があるタスクの数が実際にわからない場合はどうなりますか?1000個のタスクを実行する必要がある場合はどうなりますか?最初のものはあまり読みにくいでしょうawait t1; await t2; ....; await tn=> 2番目のものはどちらの場合も常に最良の選択です
cuongle

あなたのコメントは理にかなっています。私が最近答えた別の質問に関連して、私は自分のために何かを明確にしようとしていまし。その場合、3つのタスクがありました。
2013

回答:


112

はい、使用します WhenAll一度にすべてのエラーを伝播ためしてください。複数の待機があるため、前の待機の1つがスローすると、エラーが失われます。

もう1つの重要な違いは、障害(障害またはキャンセルされたタスク)がある場合でも、WhenAllすべてのタスクが完了するのを待つことです。手動で順番に待機すると、プログラムの待機したい部分が実際には早く続くため、予期しない並行処理が発生します。

また、必要なセマンティクスがコードに直接文書化されているため、コードを読みやすくなると思います。


9
「すべてのエラーを一度に伝播するため」await結果がそうでない場合。
2013

2
Taskで例外がどのように管理されるかという問題に関しては、この記事はその背後にある推論についてすばやくしかし優れた洞察を提供します(そして、偶然にも、複数の待機とは対照的にWhenAllの利点についての通告をします):ブログ.msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg

5
@OskarLindberg OPは、最初のタスクを待つ前に、すべてのタスクを開始しています。したがって、それらは同時に実行されます。リンクをありがとう。
usr

3
@usr私は、WhenAllが同じSynchronizationContextを維持するなどの賢いことを行わず、セマンティクスとは別にその利点をさらにプッシュするかどうかを知りたいと思っていました。決定的なドキュメントは見つかりませんでしたが、ILを見ると、明らかに異なるIAsyncStateMachineの実装があります。私はILをそれほどよく読んでいませんが、少なくともWhenAllはより効率的なILコードを生成するように見えます。(いずれにせよ、WhenAllの結果が私に関係するすべてのタスクの状態を反映しているという事実だけが、ほとんどの場合それを好むのに十分な理由です。)
Oskar Lindberg

17
もう1つの重要な違いは、たとえば、t1またはt2が例外をスローしたりキャンセルされたりした場合でも、WhenAllはすべてのタスクが完了するまで待機することです。
Magnus

28

私の理解は、Task.WhenAll複数awaitのs よりも優先する主な理由は、パフォーマンス/タスクの「チャーン」であるDoWork1ということです。このメソッドは次のようなことを行います。

  • 与えられたコンテキストから始める
  • コンテキストを保存する
  • t1を待つ
  • 元のコンテキストを復元する
  • コンテキストを保存する
  • t2を待つ
  • 元のコンテキストを復元する
  • コンテキストを保存する
  • t3を待つ
  • 元のコンテキストを復元する

対照的に、DoWork2これを行います:

  • 与えられたコンテキストから始める
  • コンテキストを保存する
  • t1、t2、t3のすべてを待つ
  • 元のコンテキストを復元する

これがあなたの特定のケースにとって十分に大きな取引であるかどうかは、もちろん、「コンテキスト依存」です(しゃれを許してください)。


4
同期コンテキストへのメッセージの送信にはコストがかかると考えているようです。本当にそうではありません。キューに追加されるデリゲートがあり、そのキューが読み取られ、デリゲートが実行されます。これが追加するオーバーヘッドは正直なところ非常に小さいです。それは何もではありませんが、大きくもありません。非同期操作の費用は、ほとんどすべてのインスタンスでそのようなオーバーヘッドを小さくします。
2013年

同意した、それは私がどちらか一方を他方を好むと考えることができる唯一の理由でした。まあ、それに加えて、スレッドの切り替えがより大きなコストになるTask.WaitAllとの類似性。
Marcel Popescu 2013年

1
@Servy As Marcelは、本当に依存していると指摘しています。たとえば、原則としてすべてのdbタスクでawaitを使用し、そのdbがasp.netインスタンスと同じマシン上にある場合、メモリ内インインデックスで安価なdbヒットを待機する場合がありますその同期スイッチとスレッドプールのシャッフルよりも。そのようなシナリオでは、WhenAll()で全体的に大きな勝利が得られる可能性があります。
Chris Moschini、2015年

3
@ChrisMoschini DBクエリは、サーバーと同じマシン上にあるDBにヒットしても、メッセージポンプにいくつかのデリゲートを追加するオーバーヘッドよりも高速になることはありません。そのインメモリクエリは、ほぼ確実にかなり遅くなります。
サービー2015年

また、t1が遅く、t2とt3が速い場合、他の待機はすぐに戻ります。
デビッドレファエリ

18

非同期メソッドは、ステートマシンとして実装されます。ステートマシンにコンパイルされないようにメソッドを作成することは可能です。これは、ファストトラック非同期メソッドと呼ばれます。これらは次のように実装できます。

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

使用Task.WhenAllする場合、呼び出し元がすべてのタスクが完了するのを待つことができることを保証しながら、このファストトラックコードを維持することが可能です。例:

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}

7

(免責事項:この回答は、Il GriffithsのPluralsightのTPL Asyncコースから引用 /インスピレーションを得ています

WhenAllを選択するもう1つの理由は、例外処理です。

DoWorkメソッドにtry-catchブロックがあり、それらが異なるDoTaskメソッドを呼び出しているとします。

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

この場合、3つのタスクすべてが例外をスローすると、最初のタスクのみがキャッチされます。それ以降の例外は失われます。つまり、t2とt3が例外をスローした場合、t2のみがキャッチされます。など。後続のタスクの例外は監視されなくなります。

WhenAllの場合と同様-タスクの一部またはすべてが失敗した場合、結果のタスクにはすべての例外が含まれます。それでも、awaitキーワードは常に最初の例外を再スローします。したがって、他の例外は依然として事実上観測されていません。これを克服する1つの方法は、タスクWhenAllの後に空の継続を追加して、待機することです。このようにして、タスクが失敗した場合、resultプロパティは完全なAggregate Exceptionをスローします。

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}

6

この質問に対する他の答えは、なぜawait Task.WhenAll(t1, t2, t3);推奨されるのかという技術的な理由を提示します。この回答は、同じ結論に達しつつ、(@ usrが示唆する)柔らかな側からそれを検討することを目的としています。

await Task.WhenAll(t1, t2, t3); 意図を宣言し、アトミックであるため、より機能的なアプローチです。

を使用await t1; await t2; await t3;すると、チームメイト(またはおそらくあなたの将来の自己さえ)が個人間にコードを追加するのを妨げるものは何もありませんawaitステートメントの。確かに、それを1行に圧縮して本質的にそれを達成しましたが、それでも問題は解決しません。その上、ソースファイルが人間の目でスキャンしにくくなる可能性があるため、コードの特定の行に複数のステートメントを含めることは、チーム設定では一般的に不適切な形式です。

簡単に言うとawait Task.WhenAll(t1, t2, t3);、意図がより明確に伝えられ、コードの意味のある更新から発生する可能性のある、または単にマージがうまくいかなかったりする可能性のある特有のバグに対して脆弱ではないため、より保守しやすくなります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.