Debido a la proliferación de procesadores de núcleos múltiples, los picatecla desarrolladores tenemos que plantearnos seriamente el cambio de “mentalidad” para aprovechar todo el potencial de estos nuevos ordenadores.
Cualquiera que haya trabajado con Thread recordará que no era nada sencillo asignar diferentes hilos a algún proceso, recoger el resultado, mezclarlo para reconstruir la versión final, no es el caso de este artículo.
Por ello uno de mis objetivos para el nuevo año es la utilización o aprendizaje de Microsoft Parallel Extensions to .NET Framework 3.5, de momento como añadido, pero que estará integrado nativamente en el .NET Framework 4.0, que nos va a permitir trabajar a alto nivel con la programación concurrente y el paralelismo. A partir de entonces utilizaremos el concepto Task en lugar de Thread. Una Task se encargar de dividir el trabajo y lanzar un número óptimo de threads basándose en el número de núcleos que tengamos, evitando el overhead.
También provee de una nueva clase estática, Parallel, que nos permitirá bucles, que no dependan de datos compartidos entre cada iteración, dejando a Parallel Extensions que se encargue de crear las Tasks necesarias para concluir el proceso utilizando todo el potencial de los procesadores o núcleos del sistema.
Aún siendo un ejemplo trivial, se puede ver la mejora de rendimiento en el segundo bucle, siendo exponencial en función del número de iteraciones a realizar.
Sub Main()
Dim sw As Stopwatch
sw = Stopwatch.StartNew()
For iC As Integer = 0 To 30
Proceso(iC)
Next
Console.WriteLine("Tiempo consumido: " & sw.ElapsedMilliseconds.ToString())
sw = Stopwatch.StartNew()
Parallel.For(0, 31, Function(iC As Integer) Proceso(iC))
Console.WriteLine("Tiempo consumido: " & sw.ElapsedMilliseconds.ToString())
Console.ReadLine()
End Sub
Function Proceso(ByVal iC As Integer) As Boolean
Console.WriteLine("Indice: " & iC.ToString & " - " & Thread.CurrentThread.ManagedThreadId.ToString())
Thread.Sleep(ic)
End Function
En este ejemplo se puede observar el resultado, con 30 iteraciones, el tiempo del primer bucle es de 475 y del segundo 287, lo más importante es ver que el primer bucle se genera siempre en el “hilo” 9 y el segundo bucle utiliza varios “hilos” para concluir el proceso más rápidamente.
Lo más representativo es ver la gráfica de uso del CPU, se puede observa que el primer bucle (marcado en rojo), no aprovecha todo el procesador, más bien, no aprovecha todos los núcleos disponibles, sin embargo el segundo (en amarillo), mantiene durante todo el bucle el procesador al 100% exprimiendo todos los recursos existentes en ese momento.
Para este ejemplo he usado un código mucho más “vulgar” para forzar el uso del procesador.
Sub Main()
Dim sw As Stopwatch
sw = Stopwatch.StartNew()
For iC As Integer = 0 To 1000
Proceso(iC)
Next
Console.WriteLine("Tiempo consumido: " & sw.ElapsedMilliseconds.ToString())
Console.WriteLine("Fin del primer bucle, Pulsa para continuar...")
Console.ReadLine()
sw = Stopwatch.StartNew()
Parallel.For(0, 1001, Function(iC As Integer) Proceso(iC))
Console.WriteLine("Tiempo consumido: " & sw.ElapsedMilliseconds.ToString())
Console.ReadLine()
End Sub
Function Proceso(ByVal iC As Integer) As Boolean
For jC As Integer = 0 To 9999999
Next
End Function
Todas estas pruebas han sido realizadas sobre un Intel Core2 Duo T8300 a 2,40 Ghz, cuantos más núcleos disponga el ordenador o servidor sobre el que utilicemos esta técnica, más se notará el rendimiento del conjunto.
Más información en:
http://msdn.microsoft.com/en-us/concurrency/default.aspx