Parallel Extensions

For Each en Parallel Extensions

En el artículo anterior hemos podido ver como utilizar el bucle For para acelerar la ejecución de cada iteración con Parallel.For, esta claro que cuanto más lento sea el proceso que debe realizar en cada iteración y cuanto mayor sea el número de vueltas del bucle, mayor será la diferencia entre usar los diferentes núcleos del sistema o no utilizarlos.

Otra funcionalidad que provee esta nueva clase, es para acelerar la utilización de For Each, sustituyéndolo por Parallel.ForEach.

En este ejemplo podemos observar como se muestra el listado de ficheros contenidos en C:Windows, realizando una espera de 1 segundo entre cada visualización.

Sub Main()
    Dim sw As Stopwatch
    sw = Stopwatch.StartNew()

    Dim Archivos As String() = Directory.GetFiles("C:Windows")

    For Each Fichero As String In Archivos
        Mostrar(Fichero)
    Next
    Console.WriteLine("Tiempo consumido: " & sw.ElapsedMilliseconds.ToString())

    Console.WriteLine("Pulsa para iniciar...")
    Console.ReadLine()

    sw = Stopwatch.StartNew()

    Parallel.ForEach(Archivos, Function(ip As String) Mostrar(ip))

    Console.WriteLine("Tiempo consumido: " & sw.ElapsedMilliseconds.ToString())
    Console.ReadLine()
End Sub
Function Mostrar(ByVal Fichero As String) As Boolean
    Thread.Sleep(1000)
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString() & " " & Fichero)
End Function

Los tiempos obtenidos han sido de 62006 para el bucle “normal” y de 17055 para el bucle con administración automática de tareas, con esto observamos nuevamente la mejora de rendimiento que obtenemos utilizando Parallel Extensions.

Aunque para ver realmente el trabajo que realiza el procesador, deberíamos hacerle trabajar, ya que Thread.Sleep no consume procesador y no se ve claramente los núcleos que utiliza del procesador.

Os propongo que modifiquéis la función Mostrar por está que os pongo a continuación y observéis el Rendimiento de los núcleos.

Function Mostrar(ByVal Fichero As String) As Boolean
    For iC As Integer = 0 To 99999999
    Next
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString() & " " & Fichero)
End Function

Inicios con Parallel Extensions

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.

image

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.

graficaparallel

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