64

В случае, если мне не нужен порядок завершения задачи, и вам просто нужно их заполнить, должен ли я использовать await Task.WhenAll вместо нескольких await? Например, является DoWord2 ниже предпочтительным методом DoWork1 (и почему?):Почему я должен отдать предпочтение одиночному 'waitait Task.WhenAll' за несколько ожиданий?

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 задач? Первый будет мало читаемым 'wait t1; дождаться t2; ....; ждать tn' => второй всегда лучший выбор в обоих случаях –

+0

Ваш комментарий имеет смысл. Я просто пытался что-то уточнить для себя, связанный с другим вопросом, который я недавно [ответил] (http://stackoverflow.com/questions/18291299/how-to-wait-until-await-async-methods-finish/). В этом случае было 3 задачи. – avo

ответ

60

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

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

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

+4

", потому что он распространяет все ошибки сразу" Нет, если вы "ждете" его результат. – svick

+1

@svick hm это правда (и нежелательно). Я изучаю эту проблему сейчас: http://stackoverflow.com/questions/18314961/await-should-throw-aggregateexception-not-just-the-first-exception – usr

+0

Извините за поздний комментарий, но я просто оказался в " пройдите мимо ", и я думаю, что основная причина использовать WhenAll в ожидании каждой задачи связана с производительностью. В ожидании каждой задачи по отдельности эффективно «сериализует» выполнение задач, WhenAll имеет потенциал для выполнения всех задач в течение времени самой продолжительной одиночной задачи, поскольку они будут выполняться параллельно (если среда выполнения поддерживает ее) , Я думаю, что принятый ответ должен быть отредактирован, чтобы отразить это. –

18

Я понимаю, что основная причина предпочесть Task.WhenAll к множеству await сек является производительность/задача «штамповать»: метод DoWork1 делает что-то вроде этого:

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

В противоположность этому, DoWork2 делает это:

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

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

+1

Вы, кажется, думаете, что отправка сообщения в контексте синхронизации является дорогостоящим. Это действительно не так. У вас есть делегат, который добавляется в очередь, эта очередь будет прочитана и делегат будет выполнен. Накладные расходы, которые это добавляет, честно очень малы. Это не совсем так, но это тоже неважно. Затраты на любые асинхронные операции будут затмевать такие накладные расходы почти во всех случаях. – Servy

+0

Согласны, это была единственная причина, по которой я мог думать о том, чтобы предпочесть один за другим. Ну, это плюс сходство с Task.WaitAll, где переключение потоков является более значительной стоимостью. –

+1

@Servy Как Марсель указывает, что ДЕЙСТВИТЕЛЬНО зависит. Если вы используете await для всех задач db как правило, например, и что db находится на той же машине, что и экземпляр asp.net, есть случаи, когда вы будете ждать удара db, который находится в памяти в индексе, дешевле чем этот переключатель синхронизации и перетасовка потока. В этом сценарии может быть значительная общая победа с WhenAll(), поэтому ... это действительно зависит. –

10

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

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

Другие ответы на этот вопрос предлагают технические причины, по которым 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); более удобен в обслуживании, поскольку он более четко информирует о ваших намерениях и менее уязвим к специфическим ошибкам, которые могут возникнуть из-за благонамеренных обновлений кода или даже просто сливаются с ошибками.