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:
- for (int i = 0; i < itemList.Count(); i++)
- {
- //Console.WriteLine(i);
- itemList[i] += (int)Math.Cos(i);
- }
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:
- var parallelResult = Parallel.For(0, itemList.Count(), (i) =>
- {
- itemList[i] += (int)Math.Cos(i);
- });
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.
- 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:
Y aquí tenemos la representación gráfica de los resultados, donde se puede apreciar mejor la diferencia de valores:
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.