c# .net async-await task-parallel-library .net-4.5

c# - En espera de mĂșltiples tareas con diferentes resultados



.net async-await (9)

Advertencia Adelante

Solo un vistazo rápido a aquellos que visitan este y otros subprocesos similares que buscan una forma de paralelizar EntityFramework usando async + await + task tool-set : El patrón que se muestra aquí es sonido, sin embargo, cuando se trata del copo de nieve especial de EF no lo hará logre la ejecución paralela a menos que y hasta que use una instancia de db-contexto separada (nueva) dentro de cada una de las llamadas * Async () involucradas.

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

Aprovechando las respuestas ya dadas, esta es la manera de asegurarse de recopilar todos los valores, incluso en el caso de que una o más de las tareas resulten en una excepción:

var car = (Car) null; var cat = (Cat) null; var house = (House) null; using (var carTask = BuyCarAsync()) using (var catTask = FeedCatAsync()) using (var houseTask = SellHouseAsync()) { try { await Task.WhenAll(carTask, catTask, houseTask); } finally { car = carTask.Status == TaskStatus.RanToCompletion ? await carTask : null; cat = catTask.Status == TaskStatus.RanToCompletion ? await catTask : null; house = houseTask.Status == TaskStatus.RanToCompletion ? await houseTask : null; } if (cat == null || car == null || house == null) throw new SomethingFishyGoingOnHereException("..."); }

La excepción agregada que contiene una o más subexcepciones seguirá siendo lanzada al final. Depende del entorno de llamadas manejarlo adecuadamente.

Tengo 3 tareas:

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

Todos deben ejecutar antes de que mi código pueda continuar y también necesito los resultados de cada uno. Ninguno de los resultados tiene algo en común entre sí

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


Dadas tres tareas: FeedCat() , SellHouse() y BuyCar() , hay dos casos interesantes: o bien 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 = FeedCat(); Task<Tesla> z = FeedCat(); // what here? }

Ahora, un enfoque simple sería:

Task.WhenAll(x, y, z);

pero ... eso no es conveniente para procesar los resultados; por lo general, deseamos await eso:

async Task<string> DoTheThings() { Task<Cat> x = FeedCat(); Task<House> y = FeedCat(); Task<Tesla> z = FeedCat(); 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 sobrecargas y asigna varias matrices (incluida la matriz params Task[] ) y listas (internamente). Funciona, pero no es excelente IMO. En muchos sentidos, es más simple usar una operación async y await cada una a su vez:

async Task<string> DoTheThings() { Task<Cat> x = FeedCat(); Task<House> y = FeedCat(); Task<Tesla> z = FeedCat(); // do something with the results... return DoWhatever(await x, await y, await z); }

Contrariamente a algunos de los comentarios anteriores, utilizar Task.WhenAll lugar de Task.WhenAll no hace ninguna diferencia en cómo se ejecutan las tareas (concurrentemente, secuencialmente, etc.). En el nivel más alto, Task.WhenAll . 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 tienes una matriz arbitraria de tareas, en lugar de 3 tareas discretas.

Pero: todavía tenemos el problema de que async / await genera mucho ruido de compilación para la continuación. Si es probable que las tareas realmente se completen sincrónicamente, entonces podemos optimizar esto construyendo en una ruta síncrona con una alternativa asincrónica:

Task<string> DoTheThings() { Task<Cat> x = FeedCat(); Task<House> y = FeedCat(); Task<Tesla> z = FeedCat(); 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 repliegue asíncrono" es cada vez más común, especialmente en el código de alto rendimiento donde las terminaciones sincrónicas son relativamente frecuentes. Tenga en cuenta que no servirá de nada si la finalización es siempre asíncrona.

Cosas adicionales que se aplican aquí:

1: con C # reciente, un patrón común es que el método de recuperación async 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 = FeedCat(); Task<Tesla> z = FeedCat(); 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: prefiera ValueTask<T> a la Task<T> si hay una gran probabilidad de que las cosas se completen de forma completamente sincronizada con muchos valores de retorno diferentes:

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 = FeedCat(); ValueTask<Tesla> z = FeedCat(); 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, prefiera IsCompletedSuccessfully to Status == TaskStatus.RanToCompletion ; esto ahora existe en .NET Core for Task , y en todas partes para ValueTask<T>


Después de usar WhenAll , puede extraer los resultados de forma individual con la 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 (dado que ya sabe que todos se han completado correctamente). Sin embargo, recomiendo usar await porque es claramente correcto, mientras que Result puede causar problemas en otros escenarios.


En caso de que intente registrar todos los errores, asegúrese de mantener la Tarea. Cuando haya toda la línea en su código, muchos comentarios sugieren que puede eliminarla y esperar por tareas individuales. Tarea. Cuando todo es realmente importante para el manejo de errores. Sin esta línea, potencialmente puede dejar 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 arroja 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ás en houseTask ni carTask. Hay 3 escenarios posibles aquí:

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

  2. SellHouse no está completo y falla con excepción en algún momento. La excepción no se observa y se volverá a lanzar en el hilo del 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 algunos de los espera se complete dentro de él. Esto sucede porque básicamente hiciste fuego y olvidaste la llamada y el contexto de sincronización se perdió tan pronto como falló FeedCat.

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 original de la pila de excepción.

Para .NET 4.0 y posterior puede capturar excepciones no observadas utilizando TaskScheduler.UnobservedTaskException. Para .NET 4.5 y excepciones no observadas más tarde se tragan por defecto para .NET 4.0 excepción no observada se bloqueará su proceso.

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


Puede almacenarlos en tareas, y aguardarlos 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;


Puede usar Task.WhenAll como 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


Si estás usando C # 7, puedes usar un método útil como este ...

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

... para habilitar una sintaxis conveniente como esta cuando desee esperar varias tareas con diferentes tipos de devolución. 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());


Simplemente await las 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;


Use Task.WhenAll y 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.