¿Por qué debería preferir single ‘aguardar Tarea.Cuando todo’ en múltiples espera?

En caso de que no me preocupe el orden de finalización de la tarea y solo necesito que completen todas, ¿debo seguir utilizando la await Task.WhenAll vez de múltiple await ? Por ejemplo, DoWord2 está debajo de un método preferido para DoWork1 (¿y 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(); } } } 

Sí, use WhenAll porque propaga todos los errores a la vez. Con la espera múltiple, se pierden los errores si uno de los primeros espera lanza.

Otra diferencia importante es que WhenAll esperará a que se completen todas las tareas. Una cadena de await abortará la espera en la primera excepción, pero la ejecución de las tareas no esperadas continúa. Esto causa concurrencia inesperada.

Creo que también hace que sea más fácil leer el código porque la semántica que desea está directamente documentada en el código.

Task.WhenAll entendido, la razón principal para preferir la Task.WhenAll a la await el rendimiento / la tarea “se DoWork1 “: el método DoWork1 hace algo como esto:

  • comenzar con un contexto dado
  • guardar el contexto
  • espera por t1
  • restaurar el contexto original
  • guardar el contexto
  • espera por t2
  • restaurar el contexto original
  • guardar el contexto
  • espera por t3
  • restaurar el contexto original

Por el contrario, DoWork2 hace esto:

  • comenzar con un contexto dado
  • guardar el contexto
  • espera por todos los t1, t2 y t3
  • restaurar el contexto original

Si este es un acuerdo lo suficientemente grande para su caso particular es, por supuesto, “dependiente del contexto” (perdón por el juego de palabras).

Un método asincrónico se implementa como una máquina de estado. Es posible escribir métodos para que no se compilen en máquinas de estado, esto a menudo se conoce como un método asincrónico de vía rápida. Estos se pueden implementar de esta manera:

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

Cuando se utiliza Task.WhenAll . Task.WhenAll que sea posible mantener este código de vía rápida sin dejar de garantizar que la persona que llama pueda esperar a que se completen todas las tareas, por ejemplo:

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

Las otras respuestas a esta pregunta ofrecen razones técnicas por las cuales await Task.WhenAll(t1, t2, t3); se prefiere. Esta respuesta tratará de verlo desde un lado más suave (al cual @usr alude) mientras se llega a la misma conclusión.

await Task.WhenAll(t1, t2, t3); es un enfoque más funcional, ya que declara intención y es atómico.

Con await t1; await t2; await t3; await t1; await t2; await t3; , no hay nada que impida que un compañero de equipo (¡o tal vez incluso tu ser futuro!) agregue código entre las declaraciones individuales en await . Claro, lo has comprimido en una línea para lograrlo, pero eso no resuelve el problema. Además, generalmente es mala forma en una configuración de equipo incluir múltiples declaraciones en una línea de código dada, ya que puede hacer que el archivo fuente sea más difícil de escanear para ojos humanos.

En pocas palabras, await Task.WhenAll(t1, t2, t3); es más fácil de mantener, ya que comunica su intención de forma más clara y es menos vulnerable a los errores peculiares que pueden surgir de las actualizaciones bien intencionadas del código, o incluso solo las fusiones que salen mal.

(Descargo de responsabilidad: Esta respuesta está tomada / inspirada del curso TPL Async de Ian Griffiths sobre Pluralsight )

Otra razón para preferir WhenAll es el manejo de excepciones.

Supongamos que tiene un bloque try-catch en sus métodos DoWork, y suponga que estaban llamando a diferentes métodos 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) { // ... } } 

En este caso, si las 3 tareas generan excepciones, solo se capturará la primera. Cualquier excepción posterior se perderá. Es decir, si t2 y t3 arrojan una excepción, solo t2 quedará atrapado; etc. Las excepciones de tareas posteriores pasarán inadvertidas.

Donde, como en el caso de WhenAll – si alguna o todas las tareas tienen un error, la tarea resultante contendrá todas las excepciones. La palabra clave await todavía arroja siempre la primera excepción. Entonces las otras excepciones aún no se observan efectivamente. Una forma de superar esto es agregar una continuación vacía después de la tarea WhenAll y poner la espera allí. De esta forma, si la tarea falla, la propiedad result arrojará la Excepción total agregada:

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