Programación paralela

Publicado por admin en

La computación paralela es una forma de cómputo en la que muchas instrucciones se ejecutan simultáneamente. Operando sobre el principio de que problemas grandes, a menudo se pueden dividir en unos más pequeños, que luego son resueltos simultáneamente.

En la ejecución de programa secuencial, las instrucciones se ejecutan en un núcleo, y el resto de núcleos no son usados. En cambio, en un programa paralelo entran en juego todos los núcleos del procesador.

PROGRAMACIÓN PARALELA CON OPENMP

OpenMP es una API para la programación multiproceso de tipo memoria compartida en múltiples plataformas como C, C++ o Fortran.

Esta disponible para múltiples arquitecturas, incluidas las plataformas Unix y de Microsoft Windows.

Se compone de un conjunto de directivas de compilador, rutinas o funciones de biblioteca y variables de entorno que influyen en el tiempo de ejecución.

  • Directivas del compilador:
    #pragma omp directiva[clausulas]

    • Ejemplo: Fijar el número de hilos.
      #pragma omp parallel num_thereads(numero)
  • Funciones de biblioteca:
    Para utilizarlas se requiere de la directiva de OpenMP: #include <omp.h>

    • omp_get_wtime() (tipo double)
      Obtiene el tiempo en segundos.
    • omp_get_thread_num() (tipo int)
      Obtiene el número de hebra
    • omp_set_num_thereads() (tipo int)
      Establece el número total de hebras
    • omp_get_num_threads() (tipo int)
      Obtiene el número real de hebras creadas
    • omp_get_num_procs() (tipo int)
      Obtiene el número de procesadores

Un ejemplo de comparación entre un programa secuencial y paralelo es el siguiente:

#include <iostream>
 
int main() {
  using namespace std;
  int id = 0;
  cout << "Hola(" << id << ") " ;
  cout << "Mundo(" << id << ")";
  return 0;
}
#include <iostream>
#include <omp.h>
 
int main() {
  using namespace std;
  #pragma omp parallel
  {
    int id = omp_get_thread_num();
    cout << "Hola(" << id << ") " ;
    cout << "Mundo(" << id << ")";
  }
  return 0;
}

TIPOS DE VARIABLES EN OPENMP

  • Variables compartidas: son accedidas por todas las hebras o hilos
  • Variables privadas: este tipo de variables solo pueden ser accedidas por un determinado hilo. Así, si se declara una variable privada para una hebra, esta no podrá ser accedida por el resto de hebras. Las variables pueden ser creadas como privadas de tres formas:
    • Las que se establecen explícitamente como privadas al declarar la directiva de inicio, con la cláusula private.
    • Los índices de instrucciones paralelas de un bucle for.
    • Las que son declaradas dentro de una sección paralela.

SINCRONIZACIÓN EN OPENMP

La sincronización en OpenMP es un mecanismo usado para establecer restricciones sobre el orden de acceso a datos compartidos.

Este tipo de restricciones son necesarias porque la velocidad de unas hebras frente a otras es impredecible, al igual, que el acceso a las variables compartidas.

Como consecuencia, el programa podría funcionar incorrectamente.

DIRECTIVAS DE SINCRONIZACIÓN

Las directivas de sincronización de alto nivel más importantes son las siguientes:

sincronización
  • Critical:
    El bloque de instrucciones no puede ser ejecutado por más de un hilo a la vez. Por lo general, la sincronización produce una pérdida de rendimiento, por lo tanto, hay que utilizar esta directiva cuando se estrictamente necesario. La sintaxis es la siguiente:

    #pragma omp critical
    {
        structured_block
    }
  • Atomic:
    La lectura y escritura de una variable se realiza por cada hebra de modo invisible. Esta directiva crea una sección crítica ligera que garantiza la actualización atómica de una posición de memoria. La sintaxis es la siguiente:

    #pragma omp atomic
    {
        assignment_statement
    }
  • Barreras
    • Barrera explícita:
      Cuando una hebra alcanza la barrera espera a que la alcancen las demás. Solo cuando todas las barreras han alcanzado la barrera, el resto de hebras pueden continuar. La sintaxis es la siguiente:

      #pragma omp barrier
    • Barreras implícitas:
      Al finalizar una sección de reparto de trabajo(worksharing).
      El concepto de “worksharing” es  compartir el trabajo de un bucle for entre los distintos hilos. Los indices que le tocan a cada hilo son privados. La sintaxis es la siguiente:

      #pragma omp for

CLÁUSULAS EN OPENMP

CLÁUSULAS DE LA DIRECTIVA PARALLEL

La directiva parallel es la directiva principal y la encargada de crear un grupo de hebras. Las principales cláusulas de la directiva parallel son las siguientes:

  • num_threads( expresión entera )
    El número de hebras creadas vendrá dado por el valor de expresión entera. Un ejemplo de utilización es el siguiente:

    #pragma omp parallel num_threads(i+50)

    Los hilos se enumeran desde 0 (hilo maestro) hasta “i+49”.

  • private( lista de variables )
    Cada hebra tendrá su copia local de cada variable de la lista, es decir, serán variables privadas.
  • shared( lista de variables )
    Solo habrá una copia compartida de cada variable de la lista compartida por todas las hebras, es decir, serán variables compartidas.
  • reduction( operador : lista de variables )
    Se utiliza cuando se puede aplicar una reducción, que consiste en obtener un resultado aplicando el mismo operador a una serie de operandos. Esta cláusula es una forma eficiente de sincronización.
    Los operadores de reducción son los siguientes:

    • (valor inicial: 0)
    • – (valor inicial: 0)
    • (valor inicial: 1)
    • (valor inicial: 0)
    • (valor inicial: 0)
    • ^ (valor inicial: 0)
    • && (valor inicial: 1)
    • || (valor inicial: 0)
  • firstprivate
    Las copias privadas de las variables se inicializan con los objetos originales, no en la directiva.
  • lastprivate
    Al salir de una región privada o lazo, la variable tiene el valor que tendría en caso de una ejecución secuencial.

CLÁUSULAS DE LA DIRECTIVA FOR

Algunas de las cláusulas anteriores también se pueden aplicar para esta directiva, como son: private, firstprivate reduction. Las cláusulas específicas de la directiva for son las siguientes:

  • nowait
    Elimina la barrera implícita al final del for de reparto de trabajo.
  • schedule( tipo,[tamaño_trozo] )
    Se podría decir que esta cláusula equivale a no utilizar la directiva de reparto de trabajo for, es decir, paralelizar el bucle de manera manual, pero con algunas opciones adicionales.

    • Los tipos principales son: static  y dynamic 
    • Cuando el tipo es static, el trabajo se divide en trozos con n interacciones, equivale al tamaño indicado. Cada trozo se asigna por orden a cada hebra, y si se llega a la última hebra se continúa con la primera.
      • Cuando no se específica el tamaño, el trabajo se divide equitativamente en trozos, siendo n el número de hebras.
    • En el caso de dynamic, el trabajo se divide igual que en el primer caso(static y con un tamaño definido) pero cada trozo se le asigna a las hebras conforme quedan libres

Deja un comentario