Почему я должен отдавать предпочтение одиночному "ожидание Task.WhenAll" в течение нескольких ожиданий?

В случае, если меня не волнует порядок завершения задачи, и мне просто нужно, чтобы все они были выполнены, следует ли мне использовать 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();
        }
    }
}

Ответ 1

Да, используйте WhenAll потому что он распространяет все ошибки одновременно. При множественном ожидании вы теряете ошибки, если один из предыдущих ожидает бросков.

Другое важное отличие состоит в том, что WhenAll будет ожидать завершения всех задач даже при наличии сбоев (сбойных или отмененных задач). Это вызывает неожиданный параллелизм, потому что часть вашей программы, которая хочет ждать, на самом деле продолжится рано.

Я думаю, что это также облегчает чтение кода, потому что семантика, которую вы хотите, прямо документирована в коде.

Ответ 2

Мое понимание состоит в том, что основной причиной предпочтительности Task.WhenAll для нескольких await является выполнение "задачи" производительности/задачи: метод DoWork1 делает что-то вроде этого:

  • начните с заданного контекста
  • сохранить контекст
  • ждать t1
  • восстановить исходный контекст
  • сохранить контекст
  • ждать t2
  • восстановить исходный контекст
  • сохранить контекст
  • ждать t3
  • восстановить исходный контекст

В отличие от этого, DoWork2 делает следующее:

  • начать с заданного контекста
  • сохранить контекст
  • ждать всех t1, t2 и t3
  • восстановить исходный контекст

Является ли это достаточно большой сделкой для вашего конкретного случая, конечно, "зависит от контекста" (помилование каламбура).

Ответ 3

Асинхронный метод реализуется как государственная машина. Можно написать методы, чтобы они не были скомпилированы в государственные машины, что часто называют быстрым методом асинхронной генерации. Они могут быть реализованы следующим образом:

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

Ответ 4

Другие ответы на этот вопрос предлагают технические причины, почему await Task.WhenAll(t1, t2, t3); является предпочтительным. Этот ответ будет направлен на то, чтобы взглянуть на него с более мягкой стороны (на что ссылается @usr), и все же прийти к тому же выводу.

await Task.WhenAll(t1, t2, t3); это более функциональный подход, поскольку он объявляет намерение и является атомарным.

С await t1; await t2; await t3; await t1; await t2; await t3; ничто не мешает партнеру по команде (или, возможно, даже самому себе!) добавить код между отдельными операторами await. Конечно, вы сжали его до одной строки, чтобы по существу этого добиться, но это не решает проблему. Кроме того, это обычно плохая форма в настройках команды, включающая несколько операторов в заданную строку кода, поскольку это может усложнить сканирование исходного файла для человеческого глаза.

Проще говоря, await Task.WhenAll(t1, t2, t3); более удобен в обслуживании, так как он более четко сообщает о ваших намерениях и менее уязвим для специфических ошибок, которые могут появиться из-за обновлений кода из лучших побуждений или даже просто ошибочных слияний.

Ответ 5

(Отказ от ответственности: Этот ответ взят/вдохновлен из курса TPL Async Яна Гриффитса по Pluralsight)

Еще одна причина, чтобы предпочесть WhenAll - обработка исключений.

Предположим, у вас есть блок try-catch для ваших методов DoWork, и предположим, что они вызывали разные методы 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 по-прежнему всегда перебрасывает первое исключение. Таким образом, другие исключения все еще остаются незамеченными. Один из способов преодолеть это - добавить пустое продолжение после задачи WhenAll и поместить туда ожидание. Таким образом, в случае сбоя задачи свойство результата выдаст полное исключение агрегирования:

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