c# tpl - Parallel.ForEach vs Task.Factory.StartNew




programming using (5)

En mi opinión, el escenario más realista es cuando las tareas tienen una operación pesada que completar. El enfoque de Shivprasad se centra más en la creación de objetos / asignación de memoria que en la informática en sí. 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 toma alrededor de 0.5 seg.

Lo llamé 200 veces usando paralelo:

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

Luego lo llamé 200 veces usando la manera antigua:

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 26656ms, el segundo en 24478ms. Lo repetí muchas veces. Cada vez que el segundo enfoque es marginalmente más rápido.

¿Cuál es la diferencia entre los siguientes fragmentos de código? ¿No estarán ambos usando hilos de hilos?

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));
}

Parallel.ForEach optimizará (puede que ni siquiera inicie nuevos subprocesos) y se bloqueará hasta que finalice el ciclo, y Task.Factory creará explícitamente una nueva instancia de tarea para cada elemento y regresará antes de que finalice (tareas asíncronas). Parallel.Foreach es mucho más eficiente.


La primera es una opción mucho mejor.

Parallel.ForEach, internamente, usa un Partitioner<T> para distribuir su colección en elementos de trabajo. No realizará una tarea por elemento, sino que se encargará de esto para reducir los gastos generales involucrados.

La segunda opción programará una sola Task por elemento en su colección. Si bien los resultados serán (casi) los mismos, esto supondrá una sobrecarga mucho mayor de la necesaria, especialmente para grandes colecciones, y hará que los tiempos de ejecución generales sean más lentos.

Para su información: el particionador utilizado se puede controlar mediante el uso de las sobrecargas apropiadas para Parallel.ForEach , si así lo desea. Para obtener más información, consulte Particiones personalizadas en MSDN.

La principal diferencia, en tiempo de ejecución, es que la segunda 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 se aprovecha de los particionadores, pero no bloquea hasta que se complete la operación.


Hice un pequeño experimento de ejecutar un método "1000000000" veces con "Parallel.For" y uno con objetos "Task".

Medí el tiempo del procesador y encontré el paralelo más eficiente. Parallel.For divide su tarea en pequeños elementos de trabajo y los ejecuta en todos los núcleos de forma paralela de una manera óptima. Mientras se crean muchos objetos de tareas (FYI TPL utilizará la agrupación de subprocesos internamente) moverá cada ejecución en cada tarea, lo que creará más tensión en el cuadro, lo que se evidencia en el experimento a continuación.

También he creado un pequeño video que explica el 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();
}


Task.Delay(0) como en la respuesta aceptada fue un buen enfoque, ya que es una copia en caché de una Task completada.

A partir del 4.6 ahora hay Task.CompletedTask que es más explícito en su propósito, pero no solo hace que Task.Delay(0) devuelva una sola instancia en caché, sino que también devuelve la misma instancia en caché que Task.CompletedTask .

Se garantiza que la naturaleza almacenada en caché de ninguno de los dos permanecerá constante, pero como optimizaciones dependientes de la implementación que solo dependen de la implementación como optimizaciones (es decir, aún funcionarían correctamente si la implementación cambiara a algo que todavía era válido) el uso de la Task.Delay(0) fue mejor que la respuesta aceptada.





c# c#-4.0 task-parallel-library parallel-extensions