¿Por qué debería preferir la única tarea 'esperar'.

127

En caso de que no me preocupe el orden de finalización de la tarea y solo necesito que todos se completen, ¿debería usar en await Task.WhenAlllugar de múltiples await? por ejemplo, ¿está DoWork2debajo de un método preferido para DoWork1(y por qué?):

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();
        }
    }
}
avo
fuente
2
¿Qué pasa si no sabes cuántas tareas necesitas hacer en paralelo? ¿Qué pasa si tienes que ejecutar 1000 tareas? El primero no será muy legible await t1; await t2; ....; await tn=> el segundo es siempre la mejor opción en ambos casos
cuongle
Tu comentario tiene sentido. Solo estaba tratando de aclarar algo por mí mismo, relacionado con otra pregunta que respondí recientemente . En ese caso, hubo 3 tareas.
avo

Respuestas:

113

Sí, utilícelo WhenAllporque propaga todos los errores a la vez. Con las esperas múltiples, pierde errores si uno de los anteriores espera lanzamientos.

Otra diferencia importante es que WhenAllesperará a que se completen todas las tareas incluso en presencia de fallas (tareas con fallas o canceladas). Esperar manualmente en secuencia provocaría una concurrencia inesperada porque la parte de su programa que quiere esperar en realidad continuará antes de tiempo.

Creo que también facilita la lectura del código porque la semántica que desea está documentada directamente en el código.

usr
fuente
9
"Porque propaga todos los errores a la vez" No si awaitsu resultado.
svick
2
En cuanto a la cuestión de cómo se gestionan las excepciones con Task, este artículo ofrece una visión rápida pero buena del razonamiento detrás de esto (y resulta que también toma nota de los beneficios de WhenAll en contraste con las múltiples esperas): blogs .msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg
55
@OskarLindberg, el OP está comenzando todas las tareas antes de esperar la primera. Entonces corren simultáneamente. Gracias por el enlace.
Usr
3
@usr Tenía curiosidad por saber si WhenAll no hace cosas inteligentes como conservar el mismo SynchronizationContext, para dejar de lado sus beneficios más allá de la semántica. No encontré documentación concluyente, pero mirando el IL hay evidentemente implementaciones diferentes de IAsyncStateMachine en juego. No leo IL tan bien, pero WhenAll al menos parece generar un código IL más eficiente. (En cualquier caso, solo el hecho de que el resultado de WhenAll refleja el estado de todas las tareas involucradas para mí es razón suficiente para preferirlo en la mayoría de los casos.)
Oskar Lindberg
17
Otra diferencia importante es que WhenAll esperará a que se completen todas las tareas, incluso si, por ejemplo, t1 o t2 arroja una excepción o se cancela.
Magnus
28

Tengo entendido que la razón principal para preferir Task.WhenAllmúltiples awaits es el rendimiento / tarea "agitación": el DoWork1método hace algo como esto:

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

Por el contrario, DoWork2hace esto:

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

Si esto es lo suficientemente importante para su caso particular es, por supuesto, "dependiente del contexto" (perdón por el juego de palabras).

Marcel Popescu
fuente
44
Parece pensar que enviar un mensaje al contexto de sincronización es costoso. Realmente no lo es. Tiene un delegado que se agrega a una cola, se leerá esa cola y se ejecutará el delegado. La sobrecarga que esto agrega es honestamente muy pequeña. No es nada, pero tampoco es grande. El gasto de cualesquiera que sean las operaciones asincrónicas reducirá la sobrecarga en casi todos los casos.
Servicio
De acuerdo, era la única razón por la que podía pensar en preferir uno sobre el otro. Bueno, eso más la similitud con Task.WaitAll donde el cambio de hilo es un costo más significativo.
Marcel Popescu
1
@Servy Como Marcel señala que REALMENTE depende. Si usas await en todas las tareas de db como una cuestión de principio, por ejemplo, y ese db se encuentra en la misma máquina que la instancia asp.net, hay casos en los que esperarás un hit de db que esté en la memoria en el índice, más barato que ese interruptor de sincronización y el conjunto de subprocesos aleatorios. Podría haber una victoria general significativa con WhenAll () en ese tipo de escenario, así que ... realmente depende.
Chris Moschini
3
@ChrisMoschini No hay forma de que la consulta de base de datos, incluso si llega a una base de datos que se encuentra en la misma máquina que el servidor, va a ser más rápida que la sobrecarga de agregar algunos delegados a una bomba de mensajes. Es casi seguro que esa consulta en memoria sea mucho más lenta.
Servy
También tenga en cuenta que si t1 es más lento y t2 y t3 son más rápidos, entonces el otro espera el retorno de inmediato.
David Refaeli
18

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 asíncrono de vía rápida. Estos pueden implementarse así:

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

Al usarlo Task.WhenAll, es posible mantener este código de vía rápida mientras se garantiza que la persona que llama puede 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);
}
Lukazoide
fuente
7

(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.

Suponga que tiene un bloque try-catch en sus métodos DoWork, y suponga que están 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 arrojan excepciones, solo se capturará la primera. Cualquier excepción posterior se perderá. Es decir, si t2 y t3 arrojan una excepción, solo se capturará t2; Las excepciones de tareas posteriores pasarán desapercibidas.

Donde como en WhenAll: si falla una o todas las tareas, la tarea resultante contendrá todas las excepciones. La palabra clave de espera siempre vuelve a arrojar la primera excepción. Entonces, las otras excepciones aún no se observan de manera efectiva. 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 manera, si la tarea falla, la propiedad del resultado arrojará la Excepción 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)
    {
        // ...
    }
}
David Refaeli
fuente
6

Las otras respuestas a esta pregunta ofrecen razones técnicas por las cuales await Task.WhenAll(t1, t2, t3);se prefiere. Esta respuesta tendrá como objetivo mirarlo desde un lado más suave (al que @usr alude) sin dejar de llegar 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;, no hay nada que impida que un compañero de equipo (¡o incluso tu futuro yo mismo!) Agregue código entre las awaitdeclaraciones individuales . Claro, lo has comprimido en una línea para lograr eso, pero eso no resuelve el problema. Además, generalmente es una mala forma en un entorno de equipo incluir varias 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 los ojos humanos.

En pocas palabras, await Task.WhenAll(t1, t2, t3);es más fácil de mantener, ya que comunica su intención con mayor claridad y es menos vulnerable a errores peculiares que pueden surgir de actualizaciones bien intencionadas del código, o incluso simplemente se fusiona mal.

rarrarrarrr
fuente