¿Cómo esperar una lista de tareas de forma asincrónica usando LINQ?

87

Tengo una lista de tareas que creé así:

public async Task<IList<Foo>> GetFoosAndDoSomethingAsync()
{
    var foos = await GetFoosAsync();

    var tasks = foos.Select(async foo => await DoSomethingAsync(foo)).ToList();

    ...
}

Al usar .ToList(), todas las tareas deberían comenzar. Ahora quiero esperar su finalización y devolver los resultados.

Esto funciona en el ...bloque anterior :

var list = new List<Foo>();
foreach (var task in tasks)
    list.Add(await task);
return list;

Hace lo que quiero, pero esto parece bastante torpe. Prefiero escribir algo más simple como esto:

return tasks.Select(async task => await task).ToList();

... pero esto no se compila. ¿Qué me estoy perdiendo? ¿O simplemente no es posible expresar las cosas de esta manera?

Matt Johnson-Pinta
fuente
¿Necesita procesar en DoSomethingAsync(foo)serie para cada foo, o es un candidato para Parallel.ForEach <Foo> ?
mdisibio
1
@mdisibio - Parallel.ForEachestá bloqueando. El patrón aquí proviene del video Asynchronous C # de Jon Skeet en Pluralsight . Se ejecuta en paralelo sin bloquear.
Matt Johnson-Pint
@mdisibio - No. Corren en paralelo. Pruébelo . (Además, parece que no lo necesito .ToList()si solo lo voy a usar WhenAll)
Matt Johnson-Pint
Punto a favor. Dependiendo de cómo DoSomethingAsyncesté escrito, la lista puede o no ejecutarse en paralelo. Pude escribir un método de prueba que era y una versión que no lo era, pero en cualquier caso, el comportamiento lo dicta el método en sí, no el delegado que crea la tarea. Lo siento por la confusión. Sin embargo, si DoSomethingAsycregresa Task<Foo>, entonces el awaiten el delegado no es absolutamente necesario ... Creo que ese era el punto principal que iba a tratar de hacer.
mdisibio

Respuestas:

136

LINQ no funciona perfectamente con asynccódigo, pero puede hacer esto:

var tasks = foos.Select(DoSomethingAsync).ToList();
await Task.WhenAll(tasks);

Si todas sus tareas devuelven el mismo tipo de valor, incluso puede hacer esto:

var results = await Task.WhenAll(tasks);

lo cual es bastante agradable. WhenAlldevuelve una matriz, por lo que creo que su método puede devolver los resultados directamente:

return await Task.WhenAll(tasks);
Stephen Cleary
fuente
11
Sólo quería señalar que esto también puede trabajar convar tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio
1
o inclusovar tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier
3
¿Cuál es la razón detrás de esto que Linq no funciona perfectamente con código asincrónico?
Ehsan Sajjad
2
@EhsanSajjad: Porque LINQ to Objects funciona sincrónicamente en objetos en memoria. Algunas cosas limitadas funcionan, como Select. Pero a la mayoría no les gusta Where.
Stephen Cleary
4
@EhsanSajjad: si la operación está basada en E / S, entonces puede usar asyncpara reducir subprocesos; si está vinculado a la CPU y ya está en un hilo de fondo, entonces asyncno proporcionaría ningún beneficio.
Stephen Cleary
9

Para ampliar la respuesta de Stephen, he creado el siguiente método de extensión para mantener el estilo fluido de LINQ. Entonces puedes hacer

await someTasks.WhenAll()

namespace System.Linq
{
    public static class IEnumerableExtensions
    {
        public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
        {
            return Task.WhenAll(source);
        }
    }
}
Clemente
fuente
10
Personalmente, nombraría su método de extensiónToArrayAsync
torvin
4

Un problema con Task.WhenAll es que crearía un paralelismo. En la mayoría de los casos, puede que sea incluso mejor, pero a veces desea evitarlo. Por ejemplo, leer datos en lotes de DB y enviar datos a algún servicio web remoto. No desea cargar todos los lotes en la memoria, pero presione la base de datos una vez que se haya procesado el lote anterior. Entonces, debes romper la asincronicidad. Aquí hay un ejemplo:

var events = Enumerable.Range(0, totalCount/ batchSize)
   .Select(x => x*batchSize)
   .Select(x => dbRepository.GetEventsBatch(x, batchSize).GetAwaiter().GetResult())
   .SelectMany(x => x);
foreach (var carEvent in events)
{
}

Tenga en cuenta que .GetAwaiter (). GetResult () lo convierte en sincronizado. DB se golpearía de forma perezosa solo una vez que se haya procesado batchSize de eventos.

Boris Lipschitz
fuente
1

Use Task.WaitAllo Task.WhenAlllo que sea apropiado.

LB
fuente
1
Eso tampoco funciona. Task.WaitAllestá bloqueando, no se puede esperar y no funcionará con un Task<T>.
Matt Johnson-Pint
@MattJohnson WhenAll?
LB
Sí. ¡Eso es! Me siento tonto. ¡Gracias!
Matt Johnson-Pint
0

Task.WhenAll debería hacer el truco aquí.

Ameen
fuente