try parallel net library example catch cancellationtoken c# .net-4.0 foreach parallel-processing

c# - net - ¿Debería siempre usar Parallel.Foreach porque más hilos DEBEN acelerar todo?



parallel foreach try catch (8)

¿Tiene sentido para ti utilizar para cada foreach normal un bucle paralelo de focach?

¿Cuándo debería comenzar a usar parallel.foreach, solo iterando 1,000,000 de elementos?


En general, una vez que va por encima de un hilo por núcleo, cada hilo adicional involucrado en una operación lo hará más lento, no más rápido.

Sin embargo, si parte de cada operación se bloquea (el ejemplo clásico está esperando en la E / S del disco o la red, y otros son productores y consumidores que no están sincronizados), entonces más hilos que núcleos pueden comenzar a acelerar las cosas nuevamente, porque las tareas se pueden realizar mientras que otros subprocesos no pueden avanzar hasta que vuelva la operación de E / S.

Por esta razón, cuando las máquinas de un solo núcleo eran la norma, las únicas justificaciones reales en multithreading eran cuando había bloqueo del tipo I / O introduce o bien mejoraba la capacidad de respuesta (un poco más lento para realizar una tarea, pero mucho más rápido para comenzar a responder a la entrada del usuario de nuevo).

Aún así, en la actualidad, las máquinas de un solo núcleo son cada vez más raras, por lo que parece que usted debería poder hacer todo al menos dos veces más rápido con el procesamiento paralelo.

Esto aún no será el caso si el orden es importante o algo inherente a la tarea lo obliga a tener un cuello de botella sincronizado, o si el número de operaciones es tan pequeño que el aumento en la velocidad del procesamiento paralelo es superado por los gastos generales involucrados en configurando ese procesamiento paralelo. Puede o no ser el caso si un recurso compartido requiere hilos para bloquear en otros hilos que realizan la misma operación paralela (dependiendo del grado de contención de bloqueo).

Además, si su código es intrínsecamente multiproceso para empezar, puede encontrarse en una situación en la que esencialmente está compitiendo por recursos consigo mismo (un caso clásico es el código ASP.NET que maneja solicitudes simultáneas). Aquí la ventaja de la operación en paralelo puede significar que una sola operación de prueba en una máquina de 4 núcleos se aproxima a 4 veces el rendimiento, pero una vez que el número de solicitudes que necesitan realizar la misma tarea llega a 4, cada una de esas 4 solicitudes es tratando de usar cada núcleo, se vuelve un poco mejor que si tuvieran un núcleo cada uno (tal vez un poco mejor, tal vez un poco peor). Por lo tanto, los beneficios de la operación paralela desaparecen a medida que el uso cambia de una prueba de solicitud única a una multitud de solicitudes en el mundo real.


Estos son mis puntos de referencia que muestran que el serial puro es el más lento, junto con varios niveles de particionamiento.

class Program { static void Main(string[] args) { NativeDllCalls(true, 1, 400000000, 0); // Seconds: 0.67 |) 595,203,995.01 ops NativeDllCalls(true, 1, 400000000, 3); // Seconds: 0.91 |) 439,052,826.95 ops NativeDllCalls(true, 1, 400000000, 4); // Seconds: 0.80 |) 501,224,491.43 ops NativeDllCalls(true, 1, 400000000, 8); // Seconds: 0.63 |) 635,893,653.15 ops NativeDllCalls(true, 4, 100000000, 0); // Seconds: 0.35 |) 1,149,359,562.48 ops NativeDllCalls(true, 400, 1000000, 0); // Seconds: 0.24 |) 1,673,544,236.17 ops NativeDllCalls(true, 10000, 40000, 0); // Seconds: 0.22 |) 1,826,379,772.84 ops NativeDllCalls(true, 40000, 10000, 0); // Seconds: 0.21 |) 1,869,052,325.05 ops NativeDllCalls(true, 1000000, 400, 0); // Seconds: 0.24 |) 1,652,797,628.57 ops NativeDllCalls(true, 100000000, 4, 0); // Seconds: 0.31 |) 1,294,424,654.13 ops NativeDllCalls(true, 400000000, 0, 0); // Seconds: 1.10 |) 364,277,890.12 ops } static void NativeDllCalls(bool useStatic, int nonParallelIterations, int parallelIterations = 0, int maxParallelism = 0) { if (useStatic) { Iterate<string, object>( (msg, cntxt) => { ServiceContracts.ForNativeCall.SomeStaticCall(msg); } , "test", null, nonParallelIterations,parallelIterations, maxParallelism ); } else { var instance = new ServiceContracts.ForNativeCall(); Iterate( (msg, cntxt) => { cntxt.SomeCall(msg); } , "test", instance, nonParallelIterations, parallelIterations, maxParallelism); } } static void Iterate<T, C>(Action<T, C> action, T testMessage, C context, int nonParallelIterations, int parallelIterations=0, int maxParallelism= 0) { var start = DateTime.UtcNow; if(nonParallelIterations == 0) nonParallelIterations = 1; // normalize values if(parallelIterations == 0) parallelIterations = 1; if (parallelIterations > 1) { ParallelOptions options; if (maxParallelism == 0) // default max parallelism options = new ParallelOptions(); else options = new ParallelOptions { MaxDegreeOfParallelism = maxParallelism }; if (nonParallelIterations > 1) { Parallel.For(0, parallelIterations, options , (j) => { for (int i = 0; i < nonParallelIterations; ++i) { action(testMessage, context); } }); } else { // no nonParallel iterations Parallel.For(0, parallelIterations, options , (j) => { action(testMessage, context); }); } } else { for (int i = 0; i < nonParallelIterations; ++i) { action(testMessage, context); } } var end = DateTime.UtcNow; Console.WriteLine("/tSeconds: {0,8:0.00} |) {1,16:0,000.00} ops", (end - start).TotalSeconds, (Math.Max(parallelIterations, 1) * nonParallelIterations / (end - start).TotalSeconds)); } }


La respuesta corta es no , no debería usar Parallel.ForEach o construcciones relacionadas en cada ciclo que pueda. Paralelo tiene una sobrecarga, que no está justificada en bucles con pocas e iteraciones rápidas. Además, la break es significativamente más compleja dentro de estos bucles.

Parallel.ForEach es una solicitud para programar el ciclo según lo considere oportuno el planificador de tareas, en función del número de iteraciones en el ciclo, el número de núcleos de CPU en el hardware y la carga actual en ese hardware. La ejecución paralela real no siempre está garantizada, y es menos probable si hay menos núcleos, el número de iteraciones es bajo y / o la carga actual es alta.

Ver también ¿Parallel.ForEach limita la cantidad de hilos activos? y ¿Paralelo.Para usar una Tarea por iteración?

La respuesta larga:

Podemos clasificar bucles por cómo caen en dos ejes:

  1. Pocas iteraciones a través de muchas iteraciones.
  2. Cada iteración es rápida hasta que cada iteración es lenta.

Un tercer factor es si las tareas varían mucho en duración: por ejemplo, si está calculando puntos en el conjunto de Mandelbrot, algunos puntos se calculan rápidamente, algunos tardan mucho más.

Cuando hay pocas iteraciones rápidas, probablemente no valga la pena utilizar la paralelización de ninguna manera, lo más probable es que termine más lento debido a los gastos generales. Incluso si la paralelización acelera un bucle pequeño y rápido en particular, es poco probable que sea de interés: las ganancias serán pequeñas y no es un cuello de botella de rendimiento en su aplicación, así que optimice la legibilidad, no el rendimiento.

Cuando un ciclo tiene muy pocas iteraciones lentas y desea más control, puede considerar usar Tareas para manejarlas, siguiendo las líneas de:

var tasks = new List<Task>(actions.Length); foreach(var action in actions) { tasks.Add(Task.Factory.StartNew(action)); } Task.WaitAll(tasks.ToArray());

Donde hay muchas iteraciones, Parallel.ForEach está en su elemento.

La documentación de Microsoft establece que

Cuando se ejecuta un bucle paralelo, el TPL divide la fuente de datos para que el bucle pueda operar en varias partes al mismo tiempo. Detrás de escena, el Programador de tareas divide la tarea en función de los recursos del sistema y la carga de trabajo. Cuando es posible, el planificador redistribuye el trabajo entre varios subprocesos y procesadores si la carga de trabajo se desequilibra.

Esta partición y reprogramación dinámica va a ser más difícil de realizar de manera efectiva a medida que disminuye el número de iteraciones de ciclo, y es más necesaria si las iteraciones varían en duración y en presencia de otras tareas que se ejecutan en la misma máquina.

Ejecuté un código.

Los resultados de la prueba a continuación muestran una máquina sin nada más que se ejecute en ella, y no hay otros subprocesos del .Net Thread Pool en uso. Esto no es típico (de hecho, en un escenario de servidor web es muy poco realista). En la práctica, es posible que no vea ninguna paralelización con un pequeño número de iteraciones.

El código de prueba es:

namespace ParallelTests { class Program { private static int Fibonacci(int x) { if (x <= 1) { return 1; } return Fibonacci(x - 1) + Fibonacci(x - 2); } private static void DummyWork() { var result = Fibonacci(10); // inspect the result so it is no optimised away. // We know that the exception is never thrown. The compiler does not. if (result > 300) { throw new Exception("failed to to it"); } } private const int TotalWorkItems = 2000000; private static void SerialWork(int outerWorkItems) { int innerLoopLimit = TotalWorkItems / outerWorkItems; for (int index1 = 0; index1 < outerWorkItems; index1++) { InnerLoop(innerLoopLimit); } } private static void InnerLoop(int innerLoopLimit) { for (int index2 = 0; index2 < innerLoopLimit; index2++) { DummyWork(); } } private static void ParallelWork(int outerWorkItems) { int innerLoopLimit = TotalWorkItems / outerWorkItems; var outerRange = Enumerable.Range(0, outerWorkItems); Parallel.ForEach(outerRange, index1 => { InnerLoop(innerLoopLimit); }); } private static void TimeOperation(string desc, Action operation) { Stopwatch timer = new Stopwatch(); timer.Start(); operation(); timer.Stop(); string message = string.Format("{0} took {1:mm}:{1:ss}.{1:ff}", desc, timer.Elapsed); Console.WriteLine(message); } static void Main(string[] args) { TimeOperation("serial work: 1", () => Program.SerialWork(1)); TimeOperation("serial work: 2", () => Program.SerialWork(2)); TimeOperation("serial work: 3", () => Program.SerialWork(3)); TimeOperation("serial work: 4", () => Program.SerialWork(4)); TimeOperation("serial work: 8", () => Program.SerialWork(8)); TimeOperation("serial work: 16", () => Program.SerialWork(16)); TimeOperation("serial work: 32", () => Program.SerialWork(32)); TimeOperation("serial work: 1k", () => Program.SerialWork(1000)); TimeOperation("serial work: 10k", () => Program.SerialWork(10000)); TimeOperation("serial work: 100k", () => Program.SerialWork(100000)); TimeOperation("parallel work: 1", () => Program.ParallelWork(1)); TimeOperation("parallel work: 2", () => Program.ParallelWork(2)); TimeOperation("parallel work: 3", () => Program.ParallelWork(3)); TimeOperation("parallel work: 4", () => Program.ParallelWork(4)); TimeOperation("parallel work: 8", () => Program.ParallelWork(8)); TimeOperation("parallel work: 16", () => Program.ParallelWork(16)); TimeOperation("parallel work: 32", () => Program.ParallelWork(32)); TimeOperation("parallel work: 64", () => Program.ParallelWork(64)); TimeOperation("parallel work: 1k", () => Program.ParallelWork(1000)); TimeOperation("parallel work: 10k", () => Program.ParallelWork(10000)); TimeOperation("parallel work: 100k", () => Program.ParallelWork(100000)); Console.WriteLine("done"); Console.ReadLine(); } } }

los resultados en una máquina con Windows 7 de 4 núcleos son:

serial work: 1 took 00:02.31 serial work: 2 took 00:02.27 serial work: 3 took 00:02.28 serial work: 4 took 00:02.28 serial work: 8 took 00:02.28 serial work: 16 took 00:02.27 serial work: 32 took 00:02.27 serial work: 1k took 00:02.27 serial work: 10k took 00:02.28 serial work: 100k took 00:02.28 parallel work: 1 took 00:02.33 parallel work: 2 took 00:01.14 parallel work: 3 took 00:00.96 parallel work: 4 took 00:00.78 parallel work: 8 took 00:00.84 parallel work: 16 took 00:00.86 parallel work: 32 took 00:00.82 parallel work: 64 took 00:00.80 parallel work: 1k took 00:00.77 parallel work: 10k took 00:00.78 parallel work: 100k took 00:00.77 done

Ejecutando el código Compilado en .Net 4 y .Net 4.5 dan los mismos resultados.

El trabajo en serie se ejecuta de la misma manera. No importa cómo lo cortes, se ejecuta en aproximadamente 2,28 segundos.

El trabajo paralelo con 1 iteración es un poco más largo que ningún paralelismo en absoluto. 2 elementos son más cortos, por lo que es 3 y con 4 o más iteraciones es todo alrededor de 0.8 segundos.

Está utilizando todos los núcleos, pero no con 100% de eficiencia. Si el trabajo en serie se dividió en 4 formas sin sobrecarga, se completaría en 0,57 segundos (2,28 / 4 = 0,57).

En otros escenarios, no vi ninguna aceleración en absoluto con dos o tres iteraciones paralelas. No tiene un control preciso sobre eso con Parallel.ForEach y el algoritmo puede decidir "particionarlos" en solo 1 fragmento y ejecutarlo en 1 núcleo si la máquina está ocupada.


No debe reemplazar ciegamente cada bucle foreach en su aplicación con el foreach paralelo. Más hilos no significa necesariamente que su aplicación funcionará más rápido. Debe dividir la tarea en tareas más pequeñas que podrían ejecutarse en paralelo si realmente desea beneficiarse de múltiples subprocesos. Si su algoritmo no es paralelizable, no obtendrá ningún beneficio.


No hay un límite inferior para realizar operaciones paralelas. Si solo tiene 2 elementos para trabajar, pero cada uno tardará un tiempo, podría tener sentido usar Parallel.ForEach . Por otro lado, si tiene 1000000 elementos pero no hacen mucho, el bucle paralelo podría no ir más rápido que el bucle normal.

Por ejemplo, escribí un programa simple para sincronizar el tiempo en bucles donde el bucle externo funcionó con un bucle for y con Parallel.ForEach . Lo sincronicé en mi laptop de 4 CPU (dual-core, hyperthreaded).

Aquí hay una carrera con solo 2 elementos para trabajar, pero cada uno lleva un tiempo:

2 outer iterations, 100000000 inner iterations: for loop: 00:00:00.1460441 ForEach : 00:00:00.0842240

Aquí hay una carrera con millones de elementos para trabajar, pero no hacen mucho:

100000000 outer iterations, 2 inner iterations: for loop: 00:00:00.0866330 ForEach : 00:00:02.1303315

La única forma real de saber es probarlo.


No, definitivamente no deberías hacer eso. El punto importante aquí no es realmente el número de iteraciones, sino el trabajo por hacer. Si su trabajo es realmente simple, ejecutar 1000000 delegados en paralelo agregará una sobrecarga enorme y probablemente sea más lento que una solución tradicional de un solo hilo. Puede evitar esto dividiendo los datos, de modo que ejecuta trozos de trabajo.

Por ejemplo, considere la situación a continuación:

Input = Enumerable.Range(1, Count).ToArray(); Result = new double[Count]; Parallel.ForEach(Input, (value, loopState, index) => { Result[index] = value*Math.PI; });

La operación aquí es tan simple, que la sobrecarga de hacer esto en paralelo empequeñecerá la ganancia de usar múltiples núcleos. Este código se ejecuta significativamente más lento que un bucle foreach regular.

Al usar una partición, podemos reducir la sobrecarga y, de hecho, observar una ganancia en el rendimiento.

Parallel.ForEach(Partitioner.Create(0, Input.Length), range => { for (var index = range.Item1; index < range.Item2; index++) { Result[index] = Input[index]*Math.PI; } });

La moral de la historia aquí es que el paralelismo es difícil y solo deberías emplear esto después de mirar de cerca la situación en cuestión. Además, debe perfilar el código antes y después de agregar el paralelismo.

Recuerde que, independientemente de cualquier ganancia potencial, el paralelismo de ganancia siempre agrega complejidad al código, por lo que si el rendimiento ya es lo suficientemente bueno, hay pocas razones para agregar la complejidad.


No, no tiene sentido para cada foreach. Algunas razones:

  • Su código puede no ser realmente paralelizable. Por ejemplo, si está utilizando los "resultados hasta ahora" para la próxima iteración y el orden es importante)
  • Si se está agregando (por ejemplo, sumando valores), entonces hay formas de usar Parallel.ForEach para esto, pero no debe hacerlo a ciegas
  • Si su trabajo se completará muy rápido de todos modos, no hay ningún beneficio, y bien puede desacelerar las cosas

Básicamente, nada en el enhebrado debe hacerse a ciegas. Piensa dónde tiene sentido paralelizar. Ah, y mida el impacto para asegurarse de que el beneficio valga la complejidad añadida. ( Será más difícil para cosas como la depuración.) TPL es genial, pero no es un almuerzo gratis.


No. Necesita comprender qué está haciendo el código y si es factible para la paralelización. Las dependencias entre sus elementos de datos pueden dificultar la paralelización, es decir, si un subproceso usa el valor calculado para el elemento anterior, debe esperar hasta que el valor se calcule de todos modos y no pueda ejecutarse en paralelo. También debe comprender su arquitectura de destino, aunque normalmente tendrá una CPU multinúcleo en casi todo lo que compre en estos días. Incluso en un solo núcleo, puede obtener algunos beneficios de más hilos, pero solo si tiene algunas tareas de bloqueo. También debe tener en cuenta que hay una sobrecarga en la creación y organización de los hilos paralelos. Si esta sobrecarga es una fracción significativa de (o más de) el tiempo que le lleva su tarea, puede reducir la velocidad.