web-dev-qa-db-ja.com

複数の待機よりも単一の「待機Task.WhenAll」を好むのはなぜですか?

タスクの完了順序を気にせず、すべて完了させる必要がある場合、複数のawaitではなくawait Task.WhenAllを使用する必要がありますか?例えば、DoWork2DoWork1よりも望ましいメソッドの下にあります(そしてその理由は?):

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();
        }
    }
}
101
avo

はい、WhenAllを使用します。すべてのエラーを一度に伝播するためです。複数の待機により、以前の待機のいずれかがスローするとエラーが失われます。

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

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

94
usr

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

  • 指定された context で始まる
  • コンテキストを保存する
  • t1を待つ
  • 元のコンテキストを復元する
  • コンテキストを保存する
  • t2を待つ
  • 元のコンテキストを復元する
  • コンテキストを保存する
  • t3を待つ
  • 元のコンテキストを復元する

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

  • 指定されたコンテキストで開始
  • コンテキストを保存する
  • t1、t2、t3のすべてを待つ
  • 元のコンテキストを復元する

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

22
Marcel Popescu

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

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);
}
14
Lukazoid

この質問に対する他の回答は、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);は保守性が高く、意図をより明確に伝え、コードの善意の更新から発生する可能性のある、または単にマージが間違っているだけのバグに対して脆弱ではありません。

4
rarrarrarrr

(免責事項:この回答は Pluralsight のIan Griffithsの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プロパティは完全な例外をスローします:

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)
    {
        // ...
    }
}
3
David Refaeli