Ir al contenido

OpenMP — memoria compartida y fork-join

OpenMP (Open Multi-Processing) es una API para programar en paralelo con memoria compartida, en C/C++ y Fortran. Tiene tres piezas:

PiezaPara quéEjemplo
Directivas de compilador (#pragma omp …)Abrir regiones paralelas, repartir bucles, sincronizar#pragma omp parallel for
Librería (omp.h)Preguntar o fijar número e id de hilos, medir tiempoomp_get_thread_num(), omp_get_wtime()
Variables de entornoControlar la ejecución sin recompilarOMP_NUM_THREADS=4, OMP_SCHEDULE

Las directivas son “comentarios con significado”: si compilas sin -fopenmp, el compilador las ignora y el programa corre secuencial. Por eso el código del curso protege las llamadas a la librería con #ifdef _OPENMP.

  • Dónde corre: una sola máquina con memoria compartida (UMA o ccNUMA): un socket/CPU o un nodo con varios sockets. No sirve entre nodos de un clúster (eso es memoria distribuida → MPI).
  • Hilo (thread): la unidad de ejecución más pequeña que el sistema operativo puede planificar. Tú decides cuántos.
  1. El programa arranca con un solo hilo: el hilo maestro (master, id 0).
  2. FORK: al encontrar #pragma omp parallel, el maestro crea un equipo de hilos que ejecutan el bloque.
  3. JOIN: al cerrar el bloque hay una barrera implícita: todos esperan al más lento, el equipo desaparece y solo sigue el maestro.

OpenMP: modelo fork-join y la primera condición de carrera

fase 0/7
maestro#pragma omp parallel (1)#pragma omp parallel (2)
Solo existe el hilo maestro (hilo 0) ejecutando código serial.
OpenMPMPI
Memoriacompartida (una máquina)distribuida (muchos nodos)
Unidadhilosprocesos
Comunicaciónleer/escribir variables compartidasmensajes (Send/Recv, colectivas)
Esfuerzo para empezarbajo: agregar #pragma a un código que ya funcionaalto: reescribir el programa
Techo de rendimientolimitado por los núcleos de un nodoescala a todo el clúster
Riesgo típicocondiciones de carreradeadlocks
OpenMP: memoria compartidaMPI: memoria distribuidahilo 0pila: id, ihilo 1pila: id, ihilo 2pila: id, ihilo 3pila: id, iuna sola memoria: a[ ], b[ ], x, cnt …todos leen y escriben lo mismo → riesgo de carreracomunicar = leer/escribir una variableproceso 0rank = 0su memorianadie más la veproceso 1rank = 1su memorianadie más la veproceso 2rank = 2su memorianadie más la vemsgmsgcomunicar = MPI_Send / MPI_Recv

Cómo leerlo: a la izquierda los 4 hilos tienen cada uno su pila (ahí viven las variables privadas, como id o el índice i), pero todos apuntan a la misma memoria: si dos escriben a la vez la misma variable hay carrera. A la derecha cada proceso MPI tiene su propia memoria; para que otro vea un dato hay que mandarlo en un mensaje.

La lámina “OMP vs MPI” lo resume con una curva rendimiento vs esfuerzo: OpenMP sube rápido pero se aplana (se acaban los núcleos). MPI cuesta más al inicio (“el código no funciona” durante un tramo) pero llega más alto, y OpenMP + MPI (programación híbrida) llega todavía más arriba.

Ventana de terminal
g++ -fopenmp Ejemp01_hello_omp.cpp -o hello # sin -fopenmp, los #pragma se ignoran
OMP_NUM_THREADS=4 ./hello # o omp_set_num_threads(4) en el código
Ejemp01_hello_omp.cpp
#include <iostream>
#include <unistd.h>
#ifdef _OPENMP
#include <omp.h>
#endif
int main() {
int id = -1, x = 10;
#pragma omp parallel shared(x) private(id)
{
#ifdef _OPENMP
id = omp_get_thread_num(); // 0, 1, 2, ... en cada hilo
#endif
sleep(1); // fuerza la carrera si id fuera shared
printf("Peekaboo desde el hilo = %d \n", id);
}
}

Con 4 hilos imprime 4 líneas Peekaboo desde el hilo = 0…3 en un orden distinto cada vez: los hilos no tienen orden garantizado.

¿Qué cambia si quitas private(id) y dejas el sleep(1)? ¿Y si además quitas el sleep?

Ver solución

Sin private(id), id es compartida (por defecto las variables declaradas fuera de la región son shared). Con el sleep(1), todos los hilos escriben su número casi a la vez, duermen 1 s y luego leen: todos imprimen el valor del último que escribió (por ejemplo, cuatro veces = 3). Sin el sleep la carrera sigue existiendo, pero es más difícil de ver: a veces sale bien y a veces no. Que funcione una vez no prueba que sea correcto.

  • Creer que #pragma omp parallel reparte el trabajo. Solo crea el equipo: todos los hilos ejecutan el bloque completo. Para repartir las iteraciones hace falta for (o parallel for).
  • Olvidar -fopenmp: compila sin error y corre con un hilo. Si el speedup es exactamente 1, revisa esto primero.
  • Esperar que los hilos impriman en orden. No hay orden garantizado entre hilos.
  • Querer usar OpenMP entre nodos de un clúster: ahí no hay memoria compartida, hace falta MPI.

Con este código, ¿cuántas veces sale cada letra y qué orden está garantizado?

printf("A\n");
#pragma omp parallel num_threads(3)
{
printf("B\n");
#pragma omp single
printf("C\n");
}
printf("D\n");
Ver solución

A 1 vez (antes del fork, solo el maestro), B 3 veces (cada hilo), C 1 vez (single: un hilo cualquiera), D 1 vez (después del join).

Garantizado: A es la primera y D la última (el join es una barrera). Las tres B y la C se mezclan, con una sola regla: la C sale después de la B del hilo que la imprime.

Autoevaluación

0/5 correctas · 0 respondidas
1. OpenMP sirve para paralelizar…
2. Al cerrar la llave de #pragma omp parallel ocurre…
3. Si compilas sin -fopenmp…
4. Una variable declarada antes de la región paralela es, por defecto…
5. El hilo maestro tiene id…

Fuente ·U4.1 Paralelismo compartido-OMP.pdf (láminas 2–12) · semana_9/Ejemp01_hello_omp.cpp