En espera de múltiples tareas con diferentes resultados

237

Tengo 3 tareas:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

Todos deben ejecutarse antes de que mi código pueda continuar y también necesito los resultados de cada uno. Ninguno de los resultados tiene nada en común

¿Cómo llamo y espero a que se completen las 3 tareas y luego obtengo los resultados?

Ian Vink
fuente
25
¿Tiene algún requisito de pedido? Es decir, ¿no quieres vender la casa hasta después de alimentar al gato?
Eric Lippert

Respuestas:

411

Después de usar WhenAll, puede extraer los resultados individualmente con await:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

También puede usar Task.Result(ya que sabe en este punto, todos se han completado con éxito). Sin embargo, recomiendo usarlo awaitporque es claramente correcto, aunque Resultpuede causar problemas en otros escenarios.

Stephen Cleary
fuente
83
Puede eliminar el WhenAllde esto por completo; Las esperas se encargarán de garantizar que no pases de las 3 tareas posteriores hasta que se completen todas las tareas.
Servicio
134
Task.WhenAll()permite ejecutar la tarea en modo paralelo . No puedo entender por qué @Servy ha sugerido eliminarlo. Sin el WhenAllse ejecutarán uno por uno
Sergey G.
87
@Sergey: las tareas comienzan a ejecutarse de inmediato. Por ejemplo, catTaskya se está ejecutando cuando regresa FeedCat. Entonces, cualquiera de los enfoques funcionará: la única pregunta es si los quiere awaituno a la vez o todos juntos. El manejo de errores es ligeramente diferente: si lo usa Task.WhenAll, lo hará awaita todos, incluso si uno de ellos falla antes.
Stephen Cleary
23
@Sergey Calling WhenAllno tiene impacto sobre cuándo se ejecutan las operaciones o cómo se ejecutan. Es solamente tiene ninguna posibilidad de efectuar la forma en que se observan los resultados. En este caso particular, la única diferencia es que un error en uno de los dos primeros métodos daría lugar a que la excepción se lanzara en esta pila de llamadas antes en mi método que el de Stephen (aunque siempre se lanzaría el mismo error, si hubiera )
Servicio
37
@Sergey: la clave es que los métodos asincrónicos siempre devuelven tareas "activas" (ya iniciadas).
Stephen Cleary
99

Solo awaitlas tres tareas por separado, después de iniciarlas todas.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Servy
fuente
8
@Bargitta No, eso es falso. Harán su trabajo en paralelo. Siéntase libre de ejecutarlo y verlo por usted mismo.
Servicio
55
La gente sigue preguntando la misma pregunta después de años ... Siento que es importante subrayar una vez más que una tarea " comienza en crear " en el cuerpo de la respuesta : tal vez ellos no se molestan en leer comentarios
99
@StephenYork Agregar Task.WhenAllcambios literalmente nada sobre el comportamiento del programa, de ninguna manera observable. Es una llamada al método puramente redundante. Puede agregarlo, si lo desea, como una opción estética, pero no cambia lo que hace el código. El tiempo de ejecución del código será idéntico con o sin esa llamada al método (bueno, técnicamente habrá una sobrecarga muy pequeña para llamar WhenAll, pero esto debería ser insignificante), solo haciendo que esa versión sea un poco más larga para ejecutarse que esta versión.
Servy
44
@StephenYork Su ejemplo ejecuta las operaciones secuencialmente por dos razones. Sus métodos asincrónicos no son asincrónicos, sino síncronos. El hecho de que tenga métodos sincrónicos que siempre devuelvan tareas ya completadas evita que se ejecuten simultáneamente. A continuación, en realidad no hace lo que se muestra en esta respuesta al iniciar los tres métodos asincrónicos y luego esperar las tres tareas por turno. Su ejemplo no llama a cada método hasta que el anterior haya terminado, evitando así explícitamente que se inicie uno hasta que el anterior haya terminado, a diferencia de este código.
Servicio
44
@MarcvanNieuwenhuijzen Eso no es cierto, como se ha discutido en los comentarios aquí y en otras respuestas. Agregar WhenAlles un cambio puramente estético. La única diferencia observable en el comportamiento es si espera a que finalicen las tareas posteriores si falla una tarea anterior, lo que generalmente no es necesario hacer. Si no cree en las numerosas explicaciones de por qué su afirmación no es verdadera, simplemente puede ejecutar el código usted mismo y ver que no es cierto.
Servy
37

Si está usando C # 7, puede usar un práctico método de envoltura como este ...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

... para habilitar una sintaxis conveniente como esta cuando desea esperar en múltiples tareas con diferentes tipos de retorno. Tendría que hacer múltiples sobrecargas para diferentes números de tareas a la espera, por supuesto.

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

Sin embargo, vea la respuesta de Marc Gravell para algunas optimizaciones sobre ValueTask y tareas ya completadas si tiene la intención de convertir este ejemplo en algo real.

Joel Mueller
fuente
Las tuplas son la única característica de C # 7 involucrada aquí. Esos definitivamente están en la versión final.
Joel Mueller
Sé sobre tuplas y c # 7. Quiero decir que no puedo encontrar el método WhenAll que devuelve tuplas. ¿Qué espacio de nombres / paquete?
Yury Scherbakov
@YuryShcherbakov Task.WhenAll()no está devolviendo una tupla. Una se está construyendo a partir de las Resultpropiedades de las tareas proporcionadas después de que la tarea devuelta se Task.WhenAll()complete.
Chris Charabaruk
2
Sugeriría reemplazar las .Resultllamadas según el razonamiento de Stephen para evitar que otras personas perpetúen la mala práctica copiando su ejemplo.
julealgon
Me pregunto por qué este método no es parte del marco? Parece muy útil ¿Se les acabó el tiempo y tuvieron que detenerse en un solo tipo de retorno?
Ian Grainger
14

Dadas tres tareas FeedCat(), SellHouse()y BuyCar()hay dos casos interesantes: o todos se completan sincrónicamente (por alguna razón, tal vez el almacenamiento en caché o un error), o no lo hacen.

Digamos que tenemos, de la pregunta:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

Ahora, un enfoque simple sería:

Task.WhenAll(x, y, z);

pero ... eso no es conveniente para procesar los resultados; normalmente nos gustaría awaiteso:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

pero esto genera muchos gastos generales y asigna varias matrices (incluida la params Task[]matriz) y listas (internamente). Funciona, pero no es una gran OMI. En muchos sentidos, es más simple usar una asyncoperación y solo awaitcada una a la vez:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

Contrariamente a algunos de los comentarios anteriores, el uso en awaitlugar de noTask.WhenAll hace ninguna diferencia en la forma en que se ejecutan las tareas (simultáneamente, secuencialmente, etc.). En el nivel más alto, Task.WhenAll es anterior al buen soporte del compilador para async/ await, y fue útil cuando esas cosas no existían . También es útil cuando tiene una variedad arbitraria de tareas, en lugar de 3 tareas discretas.

Pero: todavía tenemos el problema de que async/ awaitgenera mucho ruido de compilación para la continuación. Si es probable que las tareas podrían en realidad completar de forma sincrónica, entonces podemos optimizar esto mediante la construcción de una ruta síncrona con un repliegue asíncrona:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

Este enfoque de "ruta de sincronización con recuperación asíncrona" es cada vez más común, especialmente en el código de alto rendimiento donde las terminaciones síncronas son relativamente frecuentes. Tenga en cuenta que no ayudará en absoluto si la finalización siempre es genuinamente asíncrona.

Cosas adicionales que se aplican aquí:

  1. con C # reciente, un patrón común es que el asyncmétodo alternativo se implementa comúnmente como una función local:

    Task<string> DoTheThings() {
        async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        Task<Cat> x = FeedCat();
        Task<House> y = SellHouse();
        Task<Tesla> z = BuyCar();
    
        if(x.Status == TaskStatus.RanToCompletion &&
           y.Status == TaskStatus.RanToCompletion &&
           z.Status == TaskStatus.RanToCompletion)
            return Task.FromResult(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  2. preferir ValueTask<T>a Task<T>si hay una buena probabilidad de cosas siempre totalmente sincronizada con muchos diferentes valores de retorno:

    ValueTask<string> DoTheThings() {
        async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask<Cat> x = FeedCat();
        ValueTask<House> y = SellHouse();
        ValueTask<Tesla> z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask<string>(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  3. si es posible, prefieren IsCompletedSuccessfullya Status == TaskStatus.RanToCompletion; esto ahora existe en .NET Core para Task, y en todas partes paraValueTask<T>

Marc Gravell
fuente
"Contrariamente a varias respuestas aquí, usando esperar en lugar de Tarea. Cuando todo no hace ninguna diferencia en cómo se ejecutan las tareas (concurrentemente, secuencialmente, etc.)" No veo ninguna respuesta que diga eso. Ya habría comentado sobre ellos diciendo tanto si lo hicieran. Hay muchos comentarios sobre muchas respuestas que dicen eso, pero no hay respuestas. a cual te refieres? También tenga en cuenta que su respuesta no maneja el resultado de las tareas (o trata el hecho de que los resultados son todos de un tipo diferente). Los ha compuesto en un método que solo devuelve un Taskcuando todos están hechos sin usar los resultados.
Servicio
@Servy tienes razón, eso fue comentarios; Agregaré un ajuste para mostrar usando los resultados
Marc Gravell
Se agregó un ajuste de @Servy
Marc Gravell
Además, si va a sacar pronto las tareas síncronas, también puede manejar cualquier tarea que se cancele o falle sincrónicamente, en lugar de solo aquellas completadas con éxito. Si ha tomado la decisión de que es una optimización que su programa necesita (lo cual será raro, pero sucederá), entonces bien podría ir hasta el final.
Servicio
@Servy, que es un tema complejo, se obtiene una semántica de excepción diferente de los dos escenarios, a la espera de desencadenar una excepción se comporta de manera diferente que acceder a .Result para desencadenar la excepción. La OMI en ese punto deberíamos awaitobtener la semántica de "mejor" excepción, suponiendo que las excepciones son raras pero significativas
Marc Gravell
12

Puede almacenarlos en tareas y luego esperarlos a todos:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;
Reed Copsey
fuente
no var catTask = FeedCat()ejecuta la función FeedCat()y almacena el resultado para catTaskhacer que la await Task.WhenAll()parte sea inútil ya que el método ya se ejecutó.
Kraang Prime
1
@sanuel si devuelven la tarea <t>, entonces no ... comienzan la apertura asíncrona, pero no la esperen
Reed Copsey
No creo que esto sea exacto, vea las discusiones bajo la respuesta de @ StephenCleary ... también vea la respuesta de Servy.
Rosdi Kasim
1
si necesito agregar .ConfigrtueAwait (falso). ¿Lo agregaría solo a Task.WhenAll o a cada camarero que sigue?
AstroSharp
@AstroSharp en general, es una buena idea agregarlo a todos ellos (si se completa el primero, se ignora efectivamente), pero en este caso, probablemente estaría bien hacer el primero, a menos que haya más asíncrono cosas que suceden después.
Reed Copsey
6

En caso de que esté tratando de registrar todos los errores, asegúrese de mantener Task. Cuando toda la línea en su código, muchos comentarios sugieren que puede eliminarlo y esperar tareas individuales. Task.WhenAll es realmente importante para el manejo de errores. Sin esta línea, posiblemente deje su código abierto para excepciones no observadas.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Imagine FeedCat lanza una excepción en el siguiente código:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

En ese caso, nunca esperará en houseTask ni en carTask. Hay 3 escenarios posibles aquí:

  1. SellHouse ya se completó correctamente cuando FeedCat falló. En este caso estás bien.

  2. SellHouse no está completo y falla con la excepción en algún momento. La excepción no se observa y se volverá a generar en el subproceso finalizador.

  3. SellHouse no está completo y contiene espera dentro de él. En caso de que su código se ejecute en ASP.NET, SellHouse fallará tan pronto como algunas de las esperas se completen dentro de él. Esto sucede porque básicamente hizo que se disparara y olvidara el contexto de llamada y sincronización se perdió tan pronto como FeedCat falló.

Aquí hay un error que obtendrá para el caso (3):

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

Para el caso (2) obtendrá un error similar pero con el seguimiento de la pila de excepción original.

Para .NET 4.0 y versiones posteriores, puede detectar excepciones no observadas utilizando TaskScheduler.UnobservedTaskException. Para .NET 4.5 y posteriores, las excepciones no observadas se tragan de manera predeterminada para .NET 4.0 la excepción no observada bloqueará su proceso.

Más detalles aquí: Manejo de excepciones de tareas en .NET 4.5

samfromlv
fuente
2

Puede usar Task.WhenAllcomo se mencionó o Task.WaitAll, dependiendo de si desea que el hilo espere. Eche un vistazo al enlace para obtener una explicación de ambos.

WaitAll vs WhenAll

christiandev
fuente
2

Use Task.WhenAlly luego espere los resultados:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.
Es notalie.
fuente
mm ... no Task.Value (¿tal vez solía existir en 2013?), más bien tCat.Result, tHouse.Result o tCar.Result
Stephen York
1

Advertencia hacia adelante

Solo un aviso rápido para aquellos que visitan este y otros subprocesos similares que buscan una forma de paralelizar EntityFramework usando el conjunto de herramientas de tarea async + await + : el patrón que se muestra aquí es sólido, sin embargo, cuando se trata del copo de nieve especial de EF, no lo hará lograr la ejecución paralela a menos y hasta que use una instancia de contexto db (nueva) separada dentro de cada una de las llamadas * Async () involucradas.

Este tipo de cosas es necesario debido a las limitaciones de diseño inherentes a los contextos ef-db que prohíben ejecutar múltiples consultas en paralelo en la misma instancia de contexto ef-db.


Aprovechando las respuestas ya dadas, esta es la forma de asegurarse de que recopila todos los valores incluso en el caso de que una o más de las tareas den como resultado una excepción:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

Una implementación alternativa que tiene más o menos las mismas características de rendimiento podría ser:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }
XDS
fuente
-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

si desea acceder a Cat, haga esto:

var ct = (Cat)dn[0];

Esto es muy simple de hacer y muy útil de usar, no hay necesidad de buscar una solución compleja.

taurio
fuente
1
Solo hay un problema con esto: dynamices el diablo. Es para interoperabilidad COM complicada y tal, y no debe usarse en ninguna situación donde no sea absolutamente necesario. Particularmente si te importa el rendimiento. O escriba seguridad. O refactorizando. O depuración.
Joel Mueller el