El blog de Andrés Pérez en Geeks.ms

[C#] Parallel.For

 

Considero que desarrollar algo, por simple que sea, requiere el máximo cuidado y esmero. Sin embargo si estamos hablando de incluir las prácticas de paralelización, debemos ser mucho más cuidadosos y conocer mejor el problema que tratamos resolver. Resolver algo de forma paralela no es trivial en ningún caso.

No obstante, .NET nos ofrece una serie de ayudas a través del framework para paralelizar nuestro código. Vamos a empezar viendo un bucle y un código muy simple:

Code Snippet
  1. for (int i = 0; i < itemList.Count(); i++)
  2.             {
  3.                 //Console.WriteLine(i);
  4.                 itemList[i] += (int)Math.Cos(i);
  5.             }

Tenemos un array de N elementos y a cada uno de ellos vamos a sumarle el coseno i-ésimo. Para paralelizar esto, tenemos que tener en cuenta el coste de la paralelización. Ese coste está derivado del esfuerzo que hay que hacer para lanzar y mantener los hilos o las tareas. Si este coste es demasiado alto (por ejemplo para bucles u operaciones muy pequeñas) compensa más usar siempre la versión secuencial. Vamos a ver cómo podemos paralelizar esto, empleando Parallel.For.

Parallel.For (y Parallel.ForEach) están incluidos dentro de Parallel-LINQ (PLINQ) por lo que no tenemos que instalar nada extra para poder usarlos. La sintaxis en este caso es sencilla:

Code Snippet
  1. var parallelResult = Parallel.For(0, itemList.Count(), (i) =>
  2.             {
  3.                 itemList[i] += (int)Math.Cos(i);
  4.             });

Pese a que actualmente dispone de 12 sobrecargas, muestro por ahora la más sencilla:

- Primero indicamos el índice de origen para la primera iteración del bucle.

- Después hasta dónde queremos llegar, de forma exclusiva.

- Por último, definimos un Action. El action es lo que se ejecutará.

Traduciendo, lo que queremos hacer es un bucle paralelo que vaya de 0 a itemList.Count() y que ejecute el coseno ié-simo de cada elemento. Fíjese como no tenemos dependencias ni variables compartidas. Más tarde veremos este punto. Nótese además como Parallel.For devuelve una variable de tipo ParallelLoopResult, que nos indica cuándo el bucle ha terminado de ejecutarse y cuando se ha roto la iteración antes de romper el bucle. Para obtener la medición de los tiempos que se muestran a continuación he usado un sencillo StopWatch y para medir la parte paralela, he usado el IsCompleted del ParallelLoopResult que me ha permitido esperar la ejecución paralela y poder cuantificar el tiempo trascurrido.

Code Snippet
  1. while (parallelResult.IsCompleted == false) ;

Por ahora comparemos resultados. Para ello simplemente he ejecutado el código anterior con distintos tamaños de lista, desde 1 elementos hasta 10.000.000. Para obtener mediciones un poco más precisas he ejecutado cada sección 4 veces y he hecho la media de los resultados, puesto que en algunos casos el resultado de las operaciones puede ser trivial debido a su bajo coste:

image

Y aquí tenemos la representación gráfica de los resultados, donde se puede apreciar mejor la diferencia de valores:

image

Podemos apreciar como en valores de un bucle con pocos elementos, el coste de paralelo normalmente es superior al de secuencial. Esto es debido al coste necesario para el mantenimiento de las task que forman la ejecución de la parte paralela. Pero a su vez notamos que en el momento en que el orden de los elementos va creciendo, el coste de la parte secuencial se vuelve exponencial mientras que la parte paralela, pese a que crece, dispone de un coste cercano al lineal.

Posted: 15/4/2012 20:45 por Andrés Pérez | con 6 comment(s) |
Archivado en: ,,
Comparte este post:

Comentarios

Rodrigo Corral ha opinado:

Interesante!!!! Eso sí, hay que tener en cuenta que los resultados dependerán y mucho según lo que hagas en el cuerpo del búcle... si por ejemplo estubieses accediendo a disco, o a una base de datos o a lo que sea... los resultados podrían ser muy diferentes.

Pero lo más importante es: antes de paralelizar, como en cualquier optimización medir... como tu has hecho.

Un saludo.

# April 15, 2012 9:59 PM

Andrés Pérez ha opinado:

Gracias Rodrigo! Efectivamente, el cuerpo del bucle es fundamental. Lo apunto para futuros post :)

# April 15, 2012 10:17 PM

Kiquenet ha opinado:

Andrés, muy interesante y sin duda puede dar para una serie de posts en cuanto a mediciones y según las tareas del bucle. También a destacar la serie de paralelización que empezó Lluis Franco, muy instructivos estos artículos-posts.

Sería interesante creo unas buenas prácticas con Parallel, incluso alguna aplicación completa de ejemplo -en codeplex- que siga esas buenas prácticas al estilo del antiguo MSDN Video, o Stock Trader de Microsoft.

En concreto, para ejemplos y testing puede ser adecuado el uso de

while (parallelResult.IsCompleted == false)

pero en una aplicación WinForms-WPF creo que una de las buenas prácticas sería que al terminar todos los hilos se notificase de alguna manera que han terminado  o notificar cada vez que finalice uno de los hilos, etc

Esperemos que MS pueda sacar una aplicación de buenas prácticas así.

Saludos.

# April 16, 2012 8:59 AM

Darío Cerredelo ha opinado:

Muy interesante, tomo nota para un proceso que tengo entre manos.

Un saludo.

# April 16, 2012 10:53 AM

Andrés Pérez ha opinado:

Gracias Kiquenet. Espero también cualquier información sobre buenas prácticas :)

Mi intención es, como bien has adivinado, en sacar una serie de post tratando el tema de la paralelización y comparando en la medida de lo posible entre diferentes alternativas paraleliables y frente a la versión secuencial de las cosas.

Un saludo.

# April 16, 2012 4:32 PM

lor56 ha opinado:

Hola Andrés:

Muy interesante, sobre todo para los que intentamos comenzar con el tema de la paralelización.

Yo por ejemplo, tengo problemas cuando uso el parallel.for en bucles anidados, no se si tienes previsto abordar este tema, pero al menos para mi resulta interesante ya que desconoaco el tema.

Un saludo y muchas gracias

# March 9, 2014 8:48 PM