Почему я должен предпочесть одиночную задачу ожидания.Когда все ' за несколько ждет?


в случае, если я не забочусь о порядке выполнения задачи и просто нужно их все завершить, я все равно должен использовать 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 77

5 ответов:

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

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

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

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

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

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

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

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

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

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 бросает исключение, только Т2 будет пойман; и т. д. Последующие исключения задач останутся незамеченными.

где, как и в WhenAll-если какая-либо или все задачи неисправны, результирующая задача будет содержать все исключения. Ключевое слово await по-прежнему всегда повторно вызывает первое исключение. Таким образом, другие исключения по-прежнему фактически не наблюдаются. Один из способов преодолеть это-добавить пустое продолжение после задачи 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)
    {
        // ...
    }
}