Por que eu deveria preferir o single ‘await Task.WhenAll’ durante várias espera?

Caso eu não me importe com a ordem de conclusão da tarefa e apenas precise que todos sejam concluídos, devo continuar a usar o await Task.WhenAll ? await Task.WhenAll vez de vários, await ? Por exemplo, é DoWord2 abaixo de um método preferido para DoWork1 (e por quê?):

 using System; using System.Threading.Tasks; namespace ConsoleApp { class Program { static async Task 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(); } } } 

Sim, use WhenAll porque ele propaga todos os erros de uma só vez. Com o múltiplo aguarda você perde erros se um dos anteriores aguarda lançamentos.

Outra diferença importante é que WhenAll esperará que todas as tarefas sejam concluídas. Uma cadeia de await abortaria a espera na primeira exceção, mas a execução das tarefas não esperadas continua. Isso causa uma concorrência inesperada.

Eu acho que isso também facilita a leitura do código porque a semântica que você quer está diretamente documentada no código.

Meu entendimento é que a principal razão para preferir Task.WhenAll a vários Task.WhenAll é performance / tarefa “churning”: o método DoWork1 faz algo assim:

  • começar com um determinado contexto
  • salve o contexto
  • espere por t1
  • restaurar o contexto original
  • salve o contexto
  • espere por t2
  • restaurar o contexto original
  • salve o contexto
  • aguarde t3
  • restaurar o contexto original

Por outro lado, DoWork2 faz isso:

  • começar com um determinado contexto
  • salve o contexto
  • aguarde todos os t1, t2 e t3
  • restaurar o contexto original

Se este é um negócio grande o suficiente para o seu caso particular, é claro, “dependente do contexto” (com o perdão do trocadilho).

Um método asynchronous é implementado como uma máquina de estado. É possível escrever methods para que eles não sejam compilados em máquinas de estado, isso é muitas vezes referido como um método asynchronous fast-track. Estes podem ser implementados da seguinte forma:

 public Task DoSomethingAsync() { return DoSomethingElseAsync(); } 

Ao usar o Task.WhenAll , é possível manter este código acelerado e, ao mesmo tempo, garantir que o chamador aguarde até que todas as tarefas sejam concluídas, por exemplo:

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

As outras respostas a esta questão oferecem razões técnicas por que await Task.WhenAll(t1, t2, t3); é preferível. Essa resposta terá como objective examiná-la de um lado mais suave (ao qual @usr alude), enquanto ainda chega à mesma conclusão.

await Task.WhenAll(t1, t2, t3); é uma abordagem mais funcional, pois declara intenção e é atômica.

Com await t1; await t2; await t3; await t1; await t2; await t3; , não há nada que impeça um colega de equipe (ou talvez o seu eu futuro!) de adicionar código entre as declarações de await individuais. Claro, você o compactou para uma linha para essencialmente conseguir isso, mas isso não resolve o problema. Além disso, geralmente é uma má forma em uma configuração de equipe include várias instruções em uma determinada linha de código, pois isso pode dificultar a varredura do arquivo de origem.

Simplificando, await Task.WhenAll(t1, t2, t3); é mais sustentável, pois comunica sua intenção com mais clareza e é menos vulnerável a bugs peculiares que podem resultar de atualizações bem-intencionadas do código, ou até mesmo de fusões erradas.

(Aviso: Esta resposta foi tirada / inspirada no curso TPL Async de Ian Griffiths no Pluralsight )

Outro motivo para preferir WhenAll é o tratamento de exceções.

Suponha que você tenha um bloco try-catch em seus methods DoWork e suponha que eles estejam chamando methods DoTask diferentes:

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

Neste caso, se todas as 3 tarefas lançarem exceções, somente a primeira será capturada. Qualquer exceção posterior será perdida. Ou seja, se t2 e t3 derem exceção, apenas t2 será capturado; As exceções subseqüentes de tarefas não serão observadas.

Onde, como no WhenAll – se alguma ou todas as tarefas falharem, a tarefa resultante conterá todas as exceções. A palavra-chave await ainda relança a primeira exceção. Então as outras exceções ainda são efetivamente não observadas. Uma maneira de superar isso é adicionar uma continuação vazia após a tarefa WhenAll e colocar a aguardar lá. Dessa forma, se a tarefa falhar, a propriedade resultante lançará a Exceção agregada completa:

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