Ejecutando tareas en paralelo

Ok, básicamente tengo un montón de tareas (10) y quiero comenzarlas todas al mismo tiempo y esperar a que se completen. Cuando esté terminado, quiero ejecutar otras tareas. Leí un montón de recursos sobre esto, pero no puedo hacerlo bien para mi caso particular …

Esto es lo que tengo actualmente (el código se ha simplificado):

public async Task RunTasks() { var tasks = new List { new Task(async () => await DoWork()), //and so on with the other 9 similar tasks } Parallel.ForEach(tasks, task => { task.Start(); }); Task.WhenAll(tasks).ContinueWith(done => { //Run the other tasks }); } //This function perform some I/O operations public async Task DoWork() { var results = await GetDataFromDatabaseAsync(); foreach (var result in results) { await ReadFromNetwork(result.Url); } } 

Así que mi problema es que cuando estoy esperando que las tareas se completen con la llamada WhenAll , me dice que todas las tareas finalizaron aunque ninguna de ellas se completó. Intenté agregar Console.WriteLine en mi página de foreach y cuando foreach la tarea de continuación, los datos siguen llegando desde mis Task previas que realmente no terminaron.

¿Qué estoy haciendo mal aquí?

Casi nunca deberías usar el constructor de Task directamente. En su caso, esa tarea solo dispara la tarea real que no puede esperar.

Simplemente puede llamar a DoWork y recuperar una tarea, almacenarla en una lista y esperar a que se completen todas las tareas. Sentido:

 tasks.Add(DoWork()); // ... await Task.WhenAll(tasks); 

Sin embargo, los métodos asíncronos se ejecutan sincrónicamente hasta que se alcanza la primera espera en una tarea incompleta. Si te preocupa que esa parte tarde demasiado, utiliza Task.Run para descargarla a otro hilo de ThreadPool y luego almacenar esa tarea en la lista:

 tasks.Add(Task.Run(() => DoWork())); // ... await Task.WhenAll(tasks); 

Esencialmente estás mezclando dos paradigmas asíncronos incompatibles; es decir, Parallel.ForEach() y async-await .

Por lo que quieras, haz uno o el otro. Por ejemplo, puede usar Parallel.For[Each]() y soltar async-await por completo. Parallel.For[Each]() solo regresará cuando todas las tareas paralelas estén completas, y luego podrá pasar a las otras tareas.

El código también tiene otros problemas:

  • marca el método asincrónico pero no espera en él (lo que espera es en el delegado, no en el método);

  • es casi seguro que desee .ConfigureAwait(false) en su espera, especialmente si no está tratando de utilizar los resultados inmediatamente en un hilo de interfaz de usuario.

Si desea ejecutar el paralelo de esas tareas en diferentes hilos utilizando TPL, puede necesitar algo como esto:

 public async Task RunTasks() { var tasks = new List> { DoWork, //... }; await Task.WhenAll(tasks.AsParallel().Select(async task => await task())); //Run the other tasks } 

Estos enfoques paralelizan solo una pequeña cantidad de código: la puesta en cola del método para el grupo de subprocesos y la devolución de una Task incompleta. Además, para una cantidad tan pequeña de tareas, la paralelización puede llevar más tiempo que simplemente ejecutar de forma asíncrona. Esto podría tener sentido solo si sus tareas realizan un trabajo más largo (sincrónico) antes de su primera espera.

Para la mayoría de los casos, una mejor forma será:

 public async Task RunTasks() { await Task.WhenAll(new [] { DoWork(), //... }); //Run the other tasks } 

A mi opinión en tu código:

  1. No debe envolver su código en Task antes de pasar a Parallel.ForEach .

  2. Solo puede await Task.WhenAll lugar de usar ContinueWith .

El método DoWork es un método de E / S asíncronas. Significa que no necesita múltiples hilos para ejecutar varios de ellos, ya que la mayoría de las veces el método esperará asíncronamente a que se complete la E / S. Un hilo es suficiente para hacer eso.

 public async Task RunTasks() { var tasks = new List { DoWork(), //and so on with the other 9 similar tasks }; await Task.WhenAll(tasks); //Run the other tasks } 

Casi nunca deberías usar el constructor de Task para crear una nueva tarea. Para crear una tarea de E / S asincrónica, simplemente llame al método async . Para crear una tarea que se ejecutará en un hilo del grupo de subprocesos, use Task.Run . Puede leer este artículo para obtener una explicación detallada de Task.Run y otras opciones para crear tareas.

Solo agregue un bloque try-catch alrededor de la Tarea. Cuando todo

NB: se lanza una instancia de System.AggregateException que actúa como un contenedor alrededor de una o más excepciones que se han producido. Esto es importante para los métodos que coordinan tareas múltiples como Task.WaitAll () y Task.WaitAny () para que la excepción AggregateException pueda ajustar todas las excepciones dentro de las tareas en ejecución que se hayan producido.

 try { Task.WaitAll(tasks.ToArray()); } catch(AggregateException ex) { foreach (Exception inner in ex.InnerExceptions) { Console.WriteLine(String.Format("Exception type {0} from {1}", inner.GetType(), inner.Source)); } }