Paralelo.ForEach vs Task.Run y ​​Task.WhenAll

158

¿Cuáles son las diferencias entre usar Parallel.ForEach o Task.Run () para iniciar un conjunto de tareas de forma asincrónica?

Versión 1:

List<string> strings = new List<string> { "s1", "s2", "s3" };
Parallel.ForEach(strings, s =>
{
    DoSomething(s);
});

Versión 2:

List<string> strings = new List<string> { "s1", "s2", "s3" };
List<Task> Tasks = new List<Task>();
foreach (var s in strings)
{
    Tasks.Add(Task.Run(() => DoSomething(s)));
}
await Task.WhenAll(Tasks);
Petter T
fuente
3
Creo que el segundo fragmento de código sería casi igual al primero si lo usaras en Task.WaitAlllugar de Task.WhenAll.
Avo
15
¡Tenga en cuenta también que el segundo realizará DoSomething ("s3") tres veces y no producirá el mismo resultado! stackoverflow.com/questions/4684320/…
Nullius
1
Posible duplicado de Parallel.ForEach vs Task.Factory.StartNew
Mohammad
@Dan: tenga en cuenta que la Versión 2 usa async / wait, lo que significa que es una pregunta diferente. Async / await se introdujo con VS 2012, 1,5 años después de que se escribiera el posible hilo duplicado.
Petter T
Podría echar un vistazo a .net Core Parallel.ForEach cuestiones
Heretic Monkey

Respuestas:

159

En este caso, el segundo método esperará asincrónicamente a que se completen las tareas en lugar de bloquearlas.

Sin embargo, hay una desventaja de usar Task.Runen un bucle Parallel.ForEach: hay una Partitionerque se crea para evitar realizar más tareas de las necesarias. Task.Runsiempre realizará una única tarea por elemento (ya que está haciendo esto), pero los Parallellotes de clase funcionan, por lo que crea menos tareas que el total de elementos de trabajo. Esto puede proporcionar un rendimiento general significativamente mejor, especialmente si el cuerpo del bucle tiene una pequeña cantidad de trabajo por elemento.

Si este es el caso, puede combinar ambas opciones escribiendo:

await Task.Run(() => Parallel.ForEach(strings, s =>
{
    DoSomething(s);
}));

Tenga en cuenta que esto también se puede escribir en esta forma más corta:

await Task.Run(() => Parallel.ForEach(strings, DoSomething));
Reed Copsey
fuente
1
Gran respuesta, me preguntaba si podría señalarme un buen material de lectura sobre este tema.
Dimitar Dimitrov
@DimitarDimitrov Para cosas generales de TPL, reedcopsey.com/series/parallelism-in-net4
Reed Copsey
1
Mi construcción Parallel.ForEach estaba bloqueando mi aplicación. Estaba realizando un procesamiento pesado de imágenes en su interior. Sin embargo, cuando agregué Task.Run (() => Parallel.ForEach (....)); Se detuvo. ¿Puedes explicar porque? Tenga en cuenta que limito las opciones paralelas a la cantidad de núcleos en el sistema.
monkeyjumps
3
¿Y si DoSomethinges así async void DoSomething?
Francesco Bonizzi
1
¿Qué hay de async Task DoSomething?
Shawn Mclean
37

La primera versión bloqueará sincrónicamente el hilo de llamada (y ejecutará algunas de las tareas en él).
Si es un subproceso de interfaz de usuario, esto congelará la interfaz de usuario.

La segunda versión ejecutará las tareas de forma asincrónica en el grupo de subprocesos y liberará el subproceso de llamada hasta que estén listas.

También hay diferencias en los algoritmos de programación utilizados.

Tenga en cuenta que su segundo ejemplo se puede acortar a

await Task.WhenAll(strings.Select(s => Task.Run(() => DoSomething(s)));
SLaks
fuente
2
no debería ser await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));? Tuve problemas al devolver tareas (en lugar de esperar), especialmente cuando las declaraciones como usingestaban involucradas para deshacerse de los objetos.
Martín Coll
Mi llamada Parallel.ForEach estaba causando el bloqueo de mi UI. Agregué Task.Run (() => Parallel.ForEach (....)); a él y resolvió estrellarse.
monkeyjumps
1

Terminé haciendo esto, ya que me pareció más fácil de leer:

  List<Task> x = new List<Task>();
  foreach(var s in myCollectionOfObject)
  {
      // Note there is no await here. Just collection the Tasks
      x.Add(s.DoSomethingAsync());
  }
  await Task.WhenAll(x);
Chris M.
fuente
De esta manera, ¿las tareas se ejecutan una tras otra o WhenAll las inicia todas a la vez?
Vinicius Gualberto
Por lo que puedo decir, todos comenzaron cuando llamo "DoSomethingAsync ()". Sin embargo, nada los bloquea hasta que se llama a WhenAll.
Chris M.
¿Quieres decir cuando se llama al primer "DoSomethingAsync ()"?
Vinicius Gualberto
1
@Aceite. Se bloqueará hasta la primera espera de DoSomethingAsync () ya que esto es lo que transferirá la ejecución de nuevo a su bucle. Si es sincrónico y devuelve una Tarea, todo el código se ejecutará uno tras otro y WhenAll esperará a que se completen todas las Tareas
Simon Belanger
0

He visto Parallel.ForEach usado inapropiadamente, y pensé que un ejemplo en esta pregunta ayudaría.

Cuando ejecute el código a continuación en una aplicación de consola, verá cómo las tareas ejecutadas en Parallel.ForEach no bloquean el hilo de llamada. Esto podría estar bien si no le importa el resultado (positivo o negativo) pero si necesita el resultado, debe asegurarse de usar Task.WhenAll.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace ParrellelEachExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var indexes = new int[] { 1, 2, 3 };

            RunExample((prefix) => Parallel.ForEach(indexes, (i) => DoSomethingAsync(i, prefix)),
                "Parallel.Foreach");

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("*You'll notice the tasks haven't run yet, because the main thread was not blocked*");
            Console.WriteLine("Press any key to start the next example...");
            Console.ReadKey();

            RunExample((prefix) => Task.WhenAll(indexes.Select(i => DoSomethingAsync(i, prefix)).ToArray()).Wait(),
                "Task.WhenAll");
            Console.WriteLine("All tasks are done.  Press any key to close...");
            Console.ReadKey();
        }

        static void RunExample(Action<string> action, string prefix)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine($"{Environment.NewLine}Starting '{prefix}'...");
            action(prefix);
            Console.WriteLine($"{Environment.NewLine}Finished '{prefix}'{Environment.NewLine}");
        }


        static async Task DoSomethingAsync(int i, string prefix)
        {
            await Task.Delay(i * 1000);
            Console.WriteLine($"Finished: {prefix}[{i}]");
        }
    }
}

Aquí está el resultado:

ingrese la descripción de la imagen aquí

Conclusión:

El uso de Parallel.ForEach con una tarea no bloqueará el hilo de llamada. Si te importa el resultado, asegúrate de esperar las tareas.

~ Saludos

Rogala
fuente