Почему я должен предпочесть single ' await Task.WhenAll за несколько ожидает?

в случае, если меня не волнует порядок завершения задачи и просто нужно, чтобы все они завершились, я должен использовать await Task.WhenAll вместо await? Е. Г,DoWord2 ниже предпочтительного метода 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();
        }
    }
}

5 ответов


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

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

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


мое понимание заключается в том, что главная причина предпочесть Task.WhenAll несколько awaits-производительность / задача "сбивание":DoWork1 метод делает что-то вроде этого:

  • начните с заданного контекст
  • сохранить контекст
  • ждать Т1
  • восстановить исходный контекст
  • сохранить контекст
  • ждать Т2
  • восстановить исходный контекст
  • сохранить контекст
  • ждать Т3
  • восстановить исходный контекст

напротив, DoWork2 это:

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

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


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

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

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

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

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

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


(отказ от ответственности: Этот ответ взят / вдохновлен из курса 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 задачи-исключения, только первый будет пойман. Любое последующее исключение будет потеряно. Т. е. если Т2 и Т3 кидает исключение, только t2 будет уловлено; etc. Последующие исключения задач останутся незамеченными.

где, как и в WhenAll-если какая-либо или все задачи неисправны, результирующая задача будет содержать все исключения. Ключевое слово await по-прежнему всегда повторно вызывает первое исключение. Таким образом, другие исключения по-прежнему остаются фактически незамеченными. Один из способов преодолеть это-добавить пустое продолжение после задачи WhenAll и поставить ожидание там. Таким образом, если задача не выполняется, свойство result выбросит полное исключение Aggregate:

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