Ejecutar múltiples tareas asíncronas y esperar a que todas se completen

265

Necesito ejecutar múltiples tareas asíncronas en una aplicación de consola y esperar a que se completen antes de continuar con el procesamiento.

Hay muchos artículos por ahí, pero parece que me confundo más cuanto más leo. He leído y entiendo los principios básicos de la biblioteca de tareas, pero claramente me falta un enlace en alguna parte.

Entiendo que es posible encadenar tareas para que comiencen después de que se complete otra (que es más o menos el escenario para todos los artículos que he leído), pero quiero que todas mis tareas se ejecuten al mismo tiempo, y quiero saber una vez Todos están completos.

¿Cuál es la implementación más simple para un escenario como este?

Daniel Minnaar
fuente

Respuestas:

441

Ambas respuestas no mencionaron lo esperado Task.WhenAll:

var task1 = DoWorkAsync();
var task2 = DoMoreWorkAsync();

await Task.WhenAll(task1, task2);

La principal diferencia entre Task.WaitAlly Task.WhenAlles que el primero se bloqueará (similar al uso Waiten una sola tarea), mientras que el segundo no se podrá esperar, lo que devolverá el control a la persona que llama hasta que finalicen todas las tareas.

Más aún, el manejo de excepciones difiere:

Task.WaitAll:

Al menos una de las instancias de la Tarea se canceló, o se lanzó una excepción durante la ejecución de al menos una de las instancias de la Tarea. Si se canceló una tarea, AggregateException contiene una OperationCanceledException en su colección InnerExceptions.

Task.WhenAll:

Si alguna de las tareas suministradas se completa en un estado con error, la tarea devuelta también se completará en un estado con error, donde sus excepciones contendrán la agregación del conjunto de excepciones sin envolver de cada una de las tareas suministradas.

Si ninguna de las tareas suministradas falló pero al menos una de ellas se canceló, la tarea devuelta finalizará en el estado Cancelado.

Si ninguna de las tareas falló y ninguna de las tareas se canceló, la tarea resultante finalizará en el estado RanToCompletion. Si la matriz / enumerable suministrada no contiene tareas, la tarea devuelta pasará inmediatamente a un estado RanToCompletion antes de que se devuelva al llamante.

Yuval Itzchakov
fuente
44
Cuando intento esto, ¿mis tareas se ejecutan secuencialmente? ¿Hay que comenzar cada tarea individualmente antes await Task.WhenAll(task1, task2);?
Zapnologica
44
@Zapnologica Task.WhenAllno inicia las tareas por ti. Debe proporcionarlos "en caliente", lo que significa que ya comenzó.
Yuval Itzchakov
2
Okay. Eso tiene sentido. Entonces, ¿qué hará tu ejemplo? ¿Porque no los has comenzado?
Zapnologica
2
@YuvalItzchakov muchas gracias! ¡Es tan simple pero me ayudó mucho hoy! Vale al menos +1000 :)
Daniel Dušek
1
@Pierre no estoy siguiendo. ¿Qué StartNewtienen que ver las nuevas tareas y el giro con esperar asincrónicamente a todos?
Yuval Itzchakov
106

Podrías crear muchas tareas como:

List<Task> TaskList = new List<Task>();
foreach(...)
{
   var LastTask = new Task(SomeFunction);
   LastTask.Start();
   TaskList.Add(LastTask);
}

Task.WaitAll(TaskList.ToArray());
Virus
fuente
48
Recomendaría WhenAll
Ravi
¿Es posible iniciar múltiples hilos nuevos, al mismo tiempo, usando la palabra clave wait en lugar de .Start ()?
Matt W
1
@MattW No, cuando usa aguardar, esperaría a que se complete. En este caso, no podrá crear un entorno multiproceso. Esta es la razón por la que todas las tareas se esperan al final del ciclo.
Virus
55
Voto negativo para futuros lectores ya que no está claro que esta es una llamada de bloqueo.
JRoughan
Consulte la respuesta aceptada para conocer los motivos por los que no debe hacer esto.
EL MOJO
26

La mejor opción que he visto es el siguiente método de extensión:

public static Task ForEachAsync<T>(this IEnumerable<T> sequence, Func<T, Task> action) {
    return Task.WhenAll(sequence.Select(action));
}

Llámalo así:

await sequence.ForEachAsync(item => item.SomethingAsync(blah));

O con una lambda asíncrona:

await sequence.ForEachAsync(async item => {
    var more = await GetMoreAsync(item);
    await more.FrobbleAsync();
});
me22
fuente
26

Puede usar el WhenAllque devolverá un tipo de retorno Tasko el WaitAllque no tiene retorno y bloqueará la ejecución de código adicional de manera simular Thread.Sleephasta que todas las tareas se completen, cancelen o fallen.

ingrese la descripción de la imagen aquí

Ejemplo

var tasks = new Task[] {
    TaskOperationOne(),
    TaskOperationTwo()
};

Task.WaitAll(tasks);
// or
await Task.WhenAll(tasks);

Si desea ejecutar las tareas en un orden práctico, puede obtener inspiración de esta respuesta.

NtFreX
fuente
lo siento por llegar tarde a la fiesta pero, ¿por qué tienes awaitpara cada operación y al mismo tiempo usas WaitAllo WhenAll? ¿No deberían estar las tareas en la Task[]inicialización await?
dee zg
@dee zg Tienes razón. La espera anterior derrota el propósito. Cambiaré mi respuesta y las eliminaré.
NtFreX
si eso es. ¡gracias por la aclaración! (voto a favor para una buena respuesta)
dee zg
8

¿Desea encadenar los Tasks, o pueden invocarse de forma paralela?

Para encadenar
Solo haz algo como

Task.Run(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);
Task.Factory.StartNew(...).ContinueWith(...).ContinueWith(...).ContinueWith(...);

y no olvide verificar la Taskinstancia anterior en cada una, ContinueWithya que podría tener errores.

Para la manera paralela
El método más simple que encontré: de lo Parallel.Invoke contrario, hay Task.WaitAllo incluso puedes usar WaitHandles para hacer una cuenta regresiva a cero acciones restantes (espera, hay una nueva clase:) CountdownEvent, o ...

Andreas Niedermair
fuente
3
Agradezco la respuesta, pero sus sugerencias podrían haberse explicado un poco más.
Daniel Minnaar
@drminnaar ¿qué otra explicación además de los enlaces a msdn con ejemplos necesitas? Ni siquiera hiciste clic en los enlaces, ¿verdad?
Andreas Niedermair
44
Hice clic en los enlaces y leí el contenido. Iba por la invocación, pero había muchos If's y But's sobre si se ejecuta de forma asincrónica o no. Estabas editando tu respuesta continuamente. El enlace WaitAll que publicó fue perfecto, pero busqué la respuesta que demostró la misma funcionalidad de una manera más rápida y fácil de leer. No se ofenda, su respuesta aún ofrece buenas alternativas a otros enfoques.
Daniel Minnaar
@drminnaar sin ofender aquí, solo tengo curiosidad :)
Andreas Niedermair
5

Así es como lo hago con una matriz Func <> :

var tasks = new Func<Task>[]
{
   () => myAsyncWork1(),
   () => myAsyncWork2(),
   () => myAsyncWork3()
};

await Task.WhenAll(tasks.Select(task => task()).ToArray()); //Async    
Task.WaitAll(tasks.Select(task => task()).ToArray()); //Or use WaitAll for Sync
DalSoft
fuente
1
¿Por qué no lo guardas como una matriz de tareas?
Talha Talip Açıkgöz
1
Si no tiene cuidado @ talha-talip-açıkgöz, ejecute las Tareas cuando no esperaba que se ejecutaran. Hacerlo como delegado de Func deja en claro su intención.
DalSoft
5

Otra respuesta más ... pero generalmente me encuentro en un caso, cuando necesito cargar datos simultáneamente y ponerlos en variables, como:

var cats = new List<Cat>();
var dog = new Dog();

var loadDataTasks = new Task[]
{
    Task.Run(async () => cats = await LoadCatsAsync()),
    Task.Run(async () => dog = await LoadDogAsync())
};

try
{
    await Task.WhenAll(loadDataTasks);
}
catch (Exception ex)
{
    // handle exception
}
Yehor Hromadskyi
fuente
1
Si LoadCatsAsync()y LoadDogAsync()son solo llamadas a la base de datos, están vinculadas a IO. Task.Run()es para trabajo vinculado a la CPU; agrega una sobrecarga innecesaria adicional si todo lo que está haciendo es esperar una respuesta del servidor de bases de datos. La respuesta aceptada de Yuval es la forma correcta para el trabajo vinculado a IO.
Stephen Kennedy
@StephenKennedy, ¿podría aclarar qué tipo de sobrecarga y cuánto puede afectar el rendimiento? ¡Gracias!
Yehor Hromadskyi
Eso sería bastante difícil de resumir en el cuadro de comentarios :) En cambio, recomiendo leer los artículos de Stephen Cleary: es un experto en estas cosas. Comience aquí: blog.stephencleary.com/2013/10/…
Stephen Kennedy
-1

He preparado un código para mostrarle cómo usar la tarea para algunos de estos escenarios.

    // method to run tasks in a parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks) {

        await Task.WhenAll(tasks);
    }
    // methode to run task one by one 
    public async Task RunMultipleTaskOneByOne(Task[] tasks)
    {
        for (int i = 0; i < tasks.Length - 1; i++)
            await tasks[i];
    }
    // method to run i task in parallel 
    public async Task RunMultipleTaskParallel(Task[] tasks, int i)
    {
        var countTask = tasks.Length;
        var remainTasks = 0;
        do
        {
            int toTake = (countTask < i) ? countTask : i;
            var limitedTasks = tasks.Skip(remainTasks)
                                    .Take(toTake);
            remainTasks += toTake;
            await RunMultipleTaskParallel(limitedTasks.ToArray());
        } while (remainTasks < countTask);
    }
sayah imad
fuente
1
¿Cómo obtener los resultados de las tareas? Por ejemplo, para combinar "filas" (de N tareas en paralelo) en una tabla de datos y vincularlo a gridview asp.net?
PreguntonCojoneroCabrón