A forma de introducción, PLinQ,
ofrece la capacidad de realizar consultas LinQ
To Objects, de forma paralela, ósea
utilizando todos los cores
(núcleos) de el microprocesador. En un primer vistazo, podemos pensar que esto
es magia, pero como veremos a continuación, no es algo que sea 100%
recomendable en todos los casos, y puede llegar a tener una serie de
consideraciones y fallos, que o bien no conocemos o no estamos acostumbrados en
la programación síncrona.
Una de las ideas que solemos tener en nuestra cabeza acerca
de la programación en paralelo, es que si poseemos una máquina con 4 cores o microprocesadores, un
trabajo que tarda en hacerse 20 segundos, debería de tardar 20/4, solo 5
segundos. Esto no es cierto, hay algunos casos en que la ejecución sobre un
único núcleo es más rápida que sobre n.
La diferencia radica, en que para poder hacer una ejecución en paralelo es
necesario particionar la información en trozos, para que cada uno de estos, sea
tratado por un núcleo y luego volver a fusionar la información. Todo este
trabajo, conlleva un sobrecoste, que en ocasiones no es conveniente.
Aquí dejo un link
con toda la información sobre el sistema de partición de datos para proceso de PLinQ y en este,
para ver la forma de procesado de una consulta en paralelo.
Recuerda que aquí tienes el indice de todos los posts del Curso de LinQ.
Antes de meternos en materia, me gustaría dejar una idea centralizada, clara y concisa de cuando se debe utilizar PLinQ, y no es otra que EN PROCESOS QUE CADA UNA DE SUS ITERACIONES CONLLEVE UN ALTO USO DEL PROCESADOR O UN Nº EXTREMADAMENTE ALTO DE ELEMENTOS. Esta reflexión la contemplaremos en más ocasiones dentro los 3 post que formarán esta tecnología de LinQ.
Ejemplo simple de consulta en
paralelo.
Nada mejor que ver un pequeño ejemplo de PLinQ, para poder comprobar la
extrema sencillez que acarrea el realizar una consulta en paralelo.
Me voy a intentar escapar un poco del típico ejemplo del
método EsPrimo (que busca
los números primos dentro de una secuencia) típico dentro del mundo de PLinQ,
para hacerlo con una clase un poco más compleja.
Como siempre, la clase modelo del ejemplo primero:
public class MiClase { public int Numero { get; set; } public long TotalSum { get; set; } public static long CalcularTotalSum(int number) { long result = 0; for (int i = 0; i < number; i++) { result += i; } return result; } }
Todo, ‘la mar de fácil’. Una clase que guarda 2 propiedades,
una para un número, y otra la suma de cada uno de los números tipo secuencia de
Fibonacci.
Aunque no sea lo más recomendable, he decidido incorporar a
esta clase un método estático
que realice el cálculo, que a efectos didácticos es más asequible de ver y
corto de escribir.
Una pieza de esta clase, se montaría de este modo:
Numero = 8
TotalSum = 28 (1 + 2 + 3 +
4 + 5 + 6 + 7)
Vamos a ver un ejemplo de ejecución simple asíncrona:
static void Main(string[] args) { Stopwatch reloj = new Stopwatch(); reloj.Start(); var items = Enumerable.Range(1, 80000) .Select(a => new MiClase { Numero = a, TotalSum = CalcularTotalSum(a) }).ToList(); reloj.Stop(); Console.WriteLine($"Ha tardado {reloj.ElapsedMilliseconds} milisegundos"); Console.Read(); }
Hemos añadido la instanciación de un objeto Stopwatch (System.Diagnostics), para medir tiempos.
Si ejecutamos así la consulta, este sería el resultado:
Para realizar la ejecución en paralelo, solamente tendremos
que llamar al método extensor de IEnumerable<T>
AsParallel.
static void Main(string[] args) { Stopwatch reloj = new Stopwatch(); reloj.Start(); var items = Enumerable.Range(1, 80000) .AsParallel() .Select(a => new MiClase { Numero = a, TotalSum = CalcularTotalSum(a) }).ToList(); reloj.Stop(); Console.WriteLine(); Console.WriteLine($" Ha tardado {reloj.ElapsedMilliseconds} milisegundos"); Console.Read(); }
El resultado … mágico, ha tardado 4 veces menos en realizar
el trabajo:
De hacer toda esta magia, se encarga la clase System.Linq.ParallelEnumerable que
está definida en el namespace System.Core.
En esta clase está implementada toda la funcionalidad de Parallel LinQ y en ella se exponen versiones para ejecución en
paralelo de todos los operadores de consultas estándar: Select, Where,
GroupBy, OrderBy, Skip,
First, etc.
Cuando utilizamos el método AsParallel, transformamos la ejecución de las consultas de
forma síncrona sobre un único núcleo en una consulta en paralelo multicore.
A modo informativo, mostraremos las firmas de sus métodos
extensores, tiene 2 versiones una genérica y otra simple:
public static ParallelQuery<TSource> AsParallel<TSource>(this IEnumerable<TSource> source) public static ParallelQuery AsParallel (this IEnumerable source)
Si en algún momento necesitamos realizar el caso contrario y
desparalelizar una consulta, existe el método extensor AsSequential, que actúa exactamente al contrario que AsParallel.
¿Qué hace .NET framework para
realizar el paralelismo?
De una forma un poco simplificada, puntualizaremos los pasos
que se dan cada vez que se paraleliza una consulta:
1.- Se analiza la consulta.
2.- Se particiona la secuencia.
3.- Se realiza la ejecución en Paralelo.
4.- Se vuelven a
unificar los datos particionados de resultado.
Análisis de la
consulta
En este análisis, se procesa un algoritmo que teniendo en
cuenta las características de la máquina en la que se ejecuta y la estructura
de los datos sobre los que se va realizar la consulta, toma una decisión,
intentando adelantarse al resultado y gestionando si es más beneficioso
paralelizar la consulta o por lo contrario es más eficaz dejarla en ejecución
simple.
Como veremos más adelante, mediante la llamada al método WithExecutionMode y la Enumeración ParallelExecutionMode, podremos
forzar al motor a que en todos los casos realice la paralelización de la
consulta.
Particionamiento
de la secuencia
Una vez que ha decidido que la consulta se va a paralelizar,
será necesario particionar la secuencia en grupos de datos, que puedan ser
gestionados por cada uno de los cores
de la máquina en paralelo.
El método en que se particiona la información, no es siempre
el mismo y según el tipo datos de la colección, el tipo de acción, etc, se
repartirá con un criterio diferente.
Ya que considero que este punto no es demasiado importante
para el desenlace del post, dejo un enlace
de la MSDN, en el que se explica de forma ampliada.
Ejecución en
Paralelo
Una vez que ya están disponibles los grupos de datos
(divididos) a tratar, estos se ejecutarán en paralelo, repartiéndose el trabajo
entre los cores del
microprocesador.
Unificación de
Datos
Como paso final, una vez que todos los hilos han finalizado
su trabajo, deben unirse para completar la secuencia de resultado. PLinQ, también permite manejar el
modo de fusión por medio del método WithMergeOptions
y la Enumeración ParallelMergeOptions,
que también veremos más adelante en más profundidad, y que puede alterar
también el rendimiento de la consulta.
AsOrdered
Como hemos visto anteriormente, dentro del plan de ejecución
de una consulta en paralelo, hay una desmembración y fusión de datos. Dentro de
este proceso, perdemos la garantía de que los datos tengan el orden inicial. El
operador AsOrdered, nos avalá
la ordenación original después de la ejecución en paralelo. Utilizándolo se
pierde algo de rendimiento, pero suele ser una perdida mínima para la mejora y
seguridad que ofrece.
public static ParallelQuery AsOrdered(this ParallelQuery source);
Como podemos ver su firma no recibe ningún parámetro (no
confundir con el parámetro con la palabra reservada this, que marca el tipo a extender – Métodos
Extensores -).
Vamos a añadir esta particularidad a nuestro ejemplo:
static void Main(string[] args) { Stopwatch reloj = new Stopwatch(); reloj.Start(); var items = Enumerable.Range(1, 80000) .AsParallel().AsOrdered() .Select(a => new MiClase { Numero = a, TotalSum = MiClase.CalcularTotalSum(a) }).ToList(); reloj.Stop(); Console.WriteLine($"Ha tardado {reloj.ElapsedMilliseconds} milisegundos"); Console.Read(); }
Al igual que en otras ocasiones, también existe un método
para obligar a desordenar los datos, este es AsUnOrdered:
public static ParallelQuery<TSource> AsUnordered<TSource>(this ParallelQuery<TSource> source);
Parallel sobre colecciones pequeñas o
livianas
El consejo de Microsoft
en estos casos es no utilizar Parallel
sobre secuencias de pocos elementos, con un uso ligero de procesamiento, ya que
la simple comprobación del análisis inicial de la consulta, hace que la perdida
de rendimiento sea de una cuantía mayor a la que acontecería de realizar la
consulta de manera simple directamente ya que como hemos comentado con
anterioridad casi con toda seguridad este análisis de cómo resultado ejecución
simple.
No hay comentarios :
Publicar un comentario