Introducción a Parallel Extensions. Parte I

Introducción

Parallel Extensions es un framework de Microsoft, aún en desarrollo para facilitar el desarrollo de aplicaciones concurrentes. Recientemente acaban de sacar la primera CTP (comunity tecnology preview) que está disponible para descarga y han solicitado feedback y comentarios en sus foros.

El funcionamiento es el común de una biblioteca, pero la implementación incorpora optimizaciones para mejorar el rendimiento de las aplicaciones multihilo liberando al programador de la tarea. Internamente se encarga de "decidir" si un trabajo debe ser ejecutado en un hilo separado o si se debe ejecutar en el propio hilo que invoca la llamada. Internamente está organizado en una serie de hilos, parecido al thread pool de .NET pero con ciertas optimizaciones, ya que existen una serie de colas internas en las que se insertan los trabajos (es algo más complicado que eso pero baste la simplificación), permitiendo que determinados hilos se "roben" las tareas entre ellos para mejorar la ocupación general de la CPU y por tanto el rendimiento del programa.

La base

Parallel.For

La base de funcionamiento de la librería está centrada en la existencia de una clase estática llamada Parallel que aglutina toda la funcionalidad de la librería, mediante funciones como Parallel.For, Parallel.ForEach o Parallel.Do. Básicamente trata de abordar el cálculo en paralelo de las estructuras For o ForEach. En cuanto a la orden Do, en un momento veremos lo que hace.

El caso de las funciones For y ForEach es directamente un sustitutivo de las directivas equivalentes del lenguaje, pero cambiadas de forma que los cálculos se realicen de forma paralela. La librería no es mágica sin embargo, debemos tener en cuenta todas las precauciones referentes al multihilo, no en cuanto a cosas tales como interbloqueos o condiciones de carrera pero si hay que asegurar que lo que hay dentro del bucle puede ejecutarse sin interferencias. Vamos a ver un par de ejemplos, primero uno correcto para ilustrar como funciona. En nuestro caso vamos a sustituir un segmento del programa en el que realizamos el cuadrado de cada uno de los elementos de una lista de enteros:

for (int i = 0; i < miLista.Count; i++)
  miLista[i] *= miLista[i];

lo sustitutimos por una llamada al método Parallel.For cuya descripción es:

public static void For(
        int fromInclusive,
        int toExclusive,
        Action<int> body
)

En el que fromInclusive es el límite bajo del bucle for includo (en nuestro caso un 0), toExclusive es el limite alto excluido (en nuestro caso miLista.Count), body es la acción a realizar (que tiene que ser del tipo Action. Por lo tanto nuestro segmento de código pasa a ser

Parallel.For(0, miLista.Count, i => miLista[i] *= miLista[i]);

(observad el uso de la expresión lambda para simplificar la llamada) que como vemos queda muy compacto y suficientemente claro. Y con este cambio relativamente simple habremos conseguido sustituir nuestro antiguo For secuencial por un for paralelo en el que el motor de PFX (que es el nombre de parallel extensions) distribuirá el trabajo dentro del For de forma automática entre una o varios hilos en función de la evaluación que haga de la carga de trabajo (por ejemplo si miLista.Count vale 10 ni siquiera lance un nuevo hilo pero con un valor de 10.000 la cosa puede cambiar).

Ahora, este ejemplo es perfecatemente correcto y funcionará perfectamente debido a que la ejecución del bucle no presenta condiciones problemáticas para la ejecución multihilo no todos los bucles se pueden sustituir de una forma tan sencilla como esta, por ejemplo si examinamos

int resultado = 0;
for (int i = 0; i < miLista.Count; i++)
  resultado += miLista[i];

no podemos sustituirla por un Parallel.For ya que la instrucción "resultado += miLista[i]" presenta problemas a la hora de ejecutarse simultaneamente por varios hilos ya que la ejecución de dicha instrucción tiene una condicion de carrera ya que sigue los pasos

  1. Leer el valor de resultado
  2. Leer el valor de miLista[i]
  3. Sumar los valores y almacenarlos en resultado

por lo que si dos hilos ejecutan simultaneamente de forma que el primero ejecuta el paso 1 y en ese momento se produce un cambio de contexto para la ejecución del otro hilo que ejecuta completamente los tres pasos (y por tanto actualiza el valor de resultado), cuando la ejecución vuelva al primer hilo, su valor de resultado que leyo justo antes de realizarse el cambio de contexto ya no es válido. PFX no es capaz de detectar este tipo de cosas (de hecho es matemáticamente demostrable que no es posible generar un algóritmo válido capaz de detectarlo para todos los casos posibles) por lo que nosotros debemos poner especial cuidado en aplicarlo tan solo sobre aquellos bucles en los que no existan "malas" condiciones multihilo.

Parallel.ForEach

Visto lo anterior el uso de ForEach es exactamente igual de sencillo pero en este caso...

List<int> res = new List<int>();
foreach(int element in miLista)
  res.Add(element * element);

se convertirá en

List<int> res = new List<int>();
Parallel.ForEach(miLista, el => res.Add(el*el));

Parallel.Do

El comando Parallel.Do es, probablemente, uno de los más útiles de los que hay actualmente disponibles en la librería de PFX. De forma resumida lo que nos permite es simplificar el paralelizado de varias acciones en distintos hilos, por ejemplo

Parallel.Do(OrdenaLista(miLista), BuscaEnBaseDeDatos());

nos permitirá ejecutar en dos hilos diferentes las acciones OrdenaLista y BuscaEnBaseDeDatos (en realidad si se ejecutan o no en dos hilos dependerá de si la librería decide que debe hacerlo o que es mejor ejecutarlo de forma secuencial). Además, la llamada no volverá hasta que ambas tareas hayan sido ejecutadas. La declaración de Parallel.Do es

public static void Do(
        params Action[] actions
)

en el que Action es una params array, por lo que podemos pasar tantas acciones como deseemos y que estas se ejecuten en paralelo.

Al igual que en el caso del For y del ForEach, no obstante, deberemos tener en cuenta las consideraciones de ejecución en paralelo que mencionabamos en su momento.

Una última nota

En un par de ocasiones he recalcado que debemos estar atentos al cambiar nuestra ejecución de secuencial a paralela ya que podemos encontrarnos con problemas. Esto no significa necesiaramente que las tareas que ejecutemos en paralelo deban ser completamente independientes en absoluto, tan solo significará que tendremos que proteger dichas secciones, por ejemplo:

void Accion1()
{
  // Hacemos cosas con recursos no compartidos
  lock(this)
  {
     // Acceder a un recurso compartido, ej
     mNumAccesos++;
  }
}

void Accion2()
{
   // Hacemos cosas con recursos no compartidos
   lock(this)
   {
     mNumAccesos++;
   }
}

Parallel.Do(Accion1, Accion2);

no dará ningún problema ya que el acceso a la zona compartida se realiza de forma segura mediante un lock del recurso y por tanto, aunque se ejecute esa zona de forma concurrente no encontraremos mayores problemas.

8
Average: 8 (1 vote)
Your rating: None