Parallel.ForEach vs Task.Factory.StartNew

267

¿Cuál es la diferencia entre los fragmentos de código a continuación? ¿No usarán ambos hilos de threadpool?

Por ejemplo, si quiero llamar a una función para cada elemento de una colección,

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}
stackoverflowuser
fuente

Respuestas:

302

La primera es una opción mucho mejor.

Parallel.ForEach, internamente, utiliza a Partitioner<T>para distribuir su colección en elementos de trabajo. No realizará una tarea por elemento, sino que la procesará por lotes para reducir la sobrecarga involucrada.

La segunda opción programará un solo Taskpor artículo en su colección. Si bien los resultados serán (casi) los mismos, esto introducirá muchos más gastos generales de los necesarios, especialmente para grandes colecciones, y hará que los tiempos de ejecución generales sean más lentos.

Para su información: el Partitioner utilizado se puede controlar mediante el uso de las sobrecargas apropiadas para Parallel.ForEach , si así lo desea. Para más detalles, vea Particionadores personalizados en MSDN.

La principal diferencia, en tiempo de ejecución, es que el segundo actuará de forma asíncrona. Esto se puede duplicar usando Parallel.ForEach haciendo:

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

Al hacer esto, aún aprovecha los particionadores, pero no bloquee hasta que se complete la operación.

Reed Copsey
fuente
8
IIRC, la partición predeterminada realizada por Parallel.ForEach también tiene en cuenta la cantidad de subprocesos de hardware disponibles, evitando que tenga que calcular la cantidad óptima de tareas para comenzar. Consulte el artículo de Microsoft Patterns of Parallel Programming ; Tiene excelentes explicaciones de todo esto.
Mal Ross
2
@Mal: Más o menos ... Ese no es el Partitioner, sino el trabajo del TaskScheduler. El TaskScheduler, por defecto, usa el nuevo ThreadPool, que ahora maneja esto muy bien.
Reed Copsey
Gracias. Sabía que debería haber salido en la advertencia "No soy un experto, pero ...". :)
Mal Ross
@ReedCopsey: ¿Cómo adjuntar tareas iniciadas a través de Parallel.ForEach a la tarea del contenedor? ¿De modo que cuando llama a .Wait () en una tarea de contenedor se cuelga hasta que se completan las tareas que se ejecutan en paralelo?
Konstantin Tarkus
1
@Tarkus Si está haciendo múltiples solicitudes, es mejor que solo use HttpClient.GetString en cada elemento de trabajo (en su bucle paralelo). No hay razón para poner una opción asíncrona dentro del ciclo ya concurrente, por lo general ...
Reed Copsey
89

Hice un pequeño experimento para ejecutar un método "1,000,000,000 (mil millones)" veces con "Paralelo.Para" y uno con objetos "Tarea".

Medí el tiempo del procesador y encontré Parallel más eficiente. Paralelo: divide su tarea en pequeños elementos de trabajo y los ejecuta en todos los núcleos de forma paralela de manera óptima. Al crear muchos objetos de tarea (FYI TPL utilizará la agrupación de subprocesos internamente) moverá cada ejecución en cada tarea creando más estrés en el cuadro, lo que es evidente en el experimento a continuación.

También creé un pequeño video que explica TPL básico y también demostré cómo Parallel.For utiliza su núcleo de manera más eficiente http://www.youtube.com/watch?v=No7QqSc5cl8 en comparación con las tareas y subprocesos normales.

Experimento 1

Parallel.For(0, 1000000000, x => Method1());

Experimento 2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

Comparación de tiempo de procesador

Shivprasad Koirala
fuente
Sería más eficiente y la razón detrás de la creación de hilos es costosa. El Experimento 2 es una muy mala práctica.
Tim
@ Georgi-it por favor, preocúpate por hablar más sobre lo que es malo
Shivprasad Koirala
3
Lo siento, mi error, debería haberlo aclarado. Me refiero a la creación de tareas en un bucle de 1000000000. La sobrecarga es inimaginable. Sin mencionar que el Paralelo no puede crear más de 63 tareas a la vez, lo que lo hace mucho más optimizado en el caso.
Georgi-it
Esto es cierto para las tareas 1000000000. Sin embargo, cuando proceso una imagen (repetidamente, haciendo zoom fractal) y hago Paralelo, ya que en líneas muchos de los núcleos están inactivos mientras espero que terminen los últimos hilos. Para hacerlo más rápido, subdividí los datos en 64 paquetes de trabajo y creé tareas para ellos. (Luego, Task.WaitAll para esperar a que se complete). La idea es hacer que los subprocesos inactivos recojan un paquete de trabajo para ayudar a terminar el trabajo en lugar de esperar que 1-2 subprocesos terminen su fragmento (Paralelo.For) asignado.
Tedd Hansen
1
¿Qué hace Mehthod1()en este ejemplo?
Zapnologica
17

Parallel.ForEach optimizará (puede que ni siquiera inicie nuevos subprocesos) y bloqueará hasta que finalice el bucle, y Task.Factory creará explícitamente una nueva instancia de tarea para cada elemento, y regresará antes de que finalicen (tareas asincrónicas). Paralelo: Foreach es mucho más eficiente.

Sogger
fuente
11

En mi opinión, el escenario más realista es cuando las tareas tienen que completar una operación pesada. El enfoque de Shivprasad se centra más en la creación de objetos / asignación de memoria que en la computación misma. Hice una investigación llamando al siguiente método:

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

La ejecución de este método lleva aproximadamente 0.5 segundos.

Lo llamé 200 veces usando Paralelo:

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

Luego lo llamé 200 veces usando la forma tradicional:

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

Primer caso completado en 26656 ms, el segundo en 24478 ms. Lo repetí muchas veces. Cada vez que el segundo enfoque es marginalmente más rápido.

usuario1089583
fuente
Usar Parallel.For es la forma tradicional. Se recomienda usar Task para unidades de trabajo que no son uniformes. Los MVP de Microsoft y los diseñadores de TPL también mencionan que el uso de Tareas usará hilos de manera más eficiente, no bloqueará tantos mientras espera que se completen otras unidades.
Suncat2000