Una de las nuevas novedades que .NET Framework 4.0 incluye es el la Task Parallel Library una serie de APIS nuevas para la programación multihilo. La idea principal de esta librería, que viene incluida en el propio framework, es que cuando tengamos que añadir paralelismo y concurrencia a nuestras aplicaciones sea de lo más sencillo.
Actualmente los procesadores ya no incrementan la velocidad en Gigahercios sino que lo que hacen es replicar el hardware haciendo que nos encontremos dentro del mismo encapsulado FPGA dos procesadores exactamente iguales con sus caches de segundo y primer nivel. Esto cambia la manera de desarrollar software porque ya no nos encontramos con procesadores más rápidos sino con procesadores con más cores, 2,4,6,8 ect.
Con este nuevo escenario tenemos los desarrolladores tenemos que empezar a paralelizar nuestro software para explotar toda la potencia de los procesadores.
Una de las cosas buenas de la TPL, es que si la maquina donde se va a ejecutar tiene 2, 4 o 8 procesadores, la TPL es capaz de escalar sin necesidad de recompilar ni configurar, es decir es capaz de usar todos los cores disponibles.
Eso quiere decir que cuanto más cores utilicemos más velocidad ganaremos, aunque esto no es siempre así, porque no todas las tareas son sensibles de ser paralelizadas. Además hay que tener en cuenta que usar la TPL añade complejidad en la ejecución de la aplicación y esto en algunas ocasiones puede hacer que se degrade el rendimiento y no lo aumente. Hablaremos de eso más adelante.
Aquí tenemos la primera toma de contacto con la TPL:
Parallel.For(startIndex, endIndex, (currentIndex) => DoSomeWork(currentIndex));
Parallel.For nos permite ejecutar de manera concurrente el cuerpo de un bucle for, haciendo que la ejecución se propague por todos los cores disponibles en el sistema. Así de sencillo.
Pero como hemos comentado antes no esto a veces aumenta el rendimiento y en otros lo degrada. Veamos porque.
Cuando realizamos un bucle normal todo el código se ejecuta de manera secuencial, es decir una instrucción detrás de otra, pero cuando estamos realizando una paralelizacion de nuestro código tenemos varios threads que están ejecutando código en el mismo instante de tiempo. Si nos ponemos a pensar cómo se podría implementar a mano un Parallel.For, lo primero que tendríamos que hacer es realizar una partición de las iteraciones para dependiendo de los procesadores que tengamos repartir el trabajo.
Si tenemos por ejemplo que recorrer una lista de 50000 elementos y tenemos 2 procesadores, podemos partir la lista de dos sublistas de 25000 y generar dos threas que se encarguen de recorrer esos elementos. Ponemos los threads a ejecutar y tenemos que sincronizar cuando los dos threads terminan.
Básicamente esto es de lo que se encarga el Parallel.For de hacer por nosotros, de una manera cómoda y elegante.
Ahora bien, ¿Cuándo no es recomendable realizar un Parallel.For?, hay una regla que funciona en la mayoría de los casos, cuando el tiempo de ejecución del cuerpo del for sea mayor o igual que el tiempo de creación de los threas y de la sincronización, también es lo mismo si tenemos colecciones pequeñas. ¿Qué quieres decir esto?, veamos un ejemplo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace ParalleFor
{
class Program
{
static void Main(string[] args)
{
new Program();
}
private List<int> GetRandomList()
{
List<int> list = new List<int>();
for (int i = 0; i < 90000000; i++)
{
list.Add(i);
}
return list;
}
public Program()
{
List<int> list = GetRandomList();
Stopwatch st = new Stopwatch();
// sum all
decimal value = 2;
st.Start();
for (int i = 0; i < list.Count; i++)
{
decimal final = value * list[i];
}
st.Stop();
Console.WriteLine(st.Elapsed);
st.Reset();
st.Start();
Parallel.For(0, list.Count, index =>
{
decimal final = value * list[(int)index];
});
st.Stop();
Console.WriteLine(st.Elapsed);
}
}
}
Si nos fijamos la lista de con la que trabajamos es realmente grande, eso significa que si paralelizamos el bucle for ganaremos en rendimiento como muestra la salida de la ejecución. Pero si tenemos una lista pequeña no ganaremos tiempo sino que perderemos porque tenemos que sincronizar los n threas para esperar a que todos terminen, además del tiempo necesario para inicializarlos.
Otro de los grandes problemas de la paralelizacion es que necesitamos estructuras para sincronizar nuestro código y tener claro los conceptos de: locks, deadlocks, race condition (data and flow), etc.
En el siguiente artículo veremos el soporte para Task, las unidades básicas de ejecución dentro de TPL.
Aquí tenéis el código fuente del ejemplo.
http://www.luisguerrero.net/downloads/parallefor.zip
Saludos. Luis.