Sincronización — critical, atomic, ordered, flush
¿Por qué cnt++ falla?
Sección titulada «¿Por qué cnt++ falla?»cnt++ son tres pasos para la CPU: leer cnt a un registro, sumar 1, escribir el registro de vuelta. Si dos hilos intercalan esos pasos, uno pisa el resultado del otro.
Cómo leerlo: el tiempo baja fila por fila. A y B leen el mismo valor 5 antes de que cualquiera escriba; los dos calculan 6 y los dos escriben 6. Hubo dos incrementos y el contador solo subió uno: es un lost update. atomic o critical impiden que B lea entre la lectura y la escritura de A.
Condición de carrera en cnt++
cnt++ parece una sola cosa, pero la CPU hace 3: leer cnt a un registro, sumar 1, escribir de vuelta. Si dos hilos se intercalan, un incremento se pierde.
#pragma omp parallel for
for (i = 0; i < n; i++)
cnt++; // LOAD, ADD, STORE: ¡3 pasos!| # | hilo | op | cnt |
|---|
Medido al ejecutarlo (4 hilos, 10⁶ incrementos, g++ -fopenmp -O0):
| Versión | Resultado | Tiempo |
|---|---|---|
| sin protección | 250 087 · 359 273 · 250 328 (cambia en cada corrida) | — |
atomic | 1 000 000 ✓ | — |
critical | 1 000 000 ✓ | ≈ 0.056 s |
reduction(+:cnt) | 1 000 000 ✓ | ≈ 0.0002 s |
critical
Sección titulada «critical»Solo un hilo a la vez ejecuta el bloque; los demás que lleguen esperan. No tiene barrera al final y no acepta cláusulas, pero sí un nombre: regiones critical con nombres distintos pueden ejecutarse a la vez (U4.2: dos colas independientes xaxis e yaxis).
#pragma omp parallel shared(x, y) private(ix_next, iy_next){ #pragma omp critical (xaxis) ix_next = dequeue(x); // nadie más saca de la cola x a la vez work(ix_next, x); #pragma omp critical (yaxis) iy_next = dequeue(y); // la cola y tiene su propio candado work(iy_next, y);}Patrón clave: acumular en privado, proteger solo al final
Sección titulada «Patrón clave: acumular en privado, proteger solo al final»#pragma omp parallel private(cnt){ int cnt = 0; // contador privado de cada hilo #pragma omp for for (int i = 0; i < n; i++) if (b[i] == 0) cnt++; // ¿#pragma omp critical aquí? No hace falta #pragma omp critical cnt_t += cnt; // una sola vez por hilo}Salida real: cnt_t: 512 (la mitad de los 1024 elementos de b son 0). Con critical dentro del for el resultado sería el mismo, pero los hilos harían fila en cada cero encontrado: el bucle quedaría casi secuencial.
Hace atómica una sola actualización de memoria: un mini-critical para la línea siguiente. Solo protege esa ubicación (x), así que distintos elementos de un arreglo pueden actualizarse en paralelo.
| Cláusula | Forma | Ejemplo |
|---|---|---|
read | v = x; | leer x sin que nadie la esté escribiendo |
write | x = expr; | escribir x de forma indivisible |
update (por defecto) | x++, x--, x op= expr, x = x op expr | x[index[i]] += work1(i); |
capture | v = x++;, v = x op= expr; | actualizar y quedarte con el valor viejo o nuevo |
#pragma omp parallel for shared(x, y, index, n)for (i = 0; i < n; i++) { #pragma omp atomic update x[index[i]] += work1(i); // varios i pueden caer en el mismo index ⇒ protegido y[i] += work2(i); // NO es atómico (atomic solo afecta la línea siguiente)}y[i] no necesita protección: cada i toca un elemento distinto. Si se usara critical en vez de atomic, todas las actualizaciones de x irían en fila, aunque cayeran en elementos distintos.
π con atomic (Ejemp03_atomic_critical.c)
Sección titulada «π con atomic (Ejemp03_atomic_critical.c)»#pragma omp parallel{ int i, id = omp_get_thread_num(), nthrds = omp_get_num_threads(); double x, sum; // privadas (declaradas dentro) for (i = id; i < num_steps; i += nthrds) { // reparto cíclico manual x = (i + 0.5) * step; sum += 4.0 / (1.0 + x * x); } #pragma omp atomic // o: #pragma omp critical pi += sum * step;}Salida real (4 hilos): pi: 3.141592653590. Cada hilo suma su parte en sum y solo una vez por hilo actualiza pi de forma protegida.
ordered
Sección titulada «ordered»Dentro de un for con la cláusula ordered, el bloque #pragma omp ordered se ejecuta en el orden secuencial de las iteraciones; el resto del cuerpo sigue en paralelo. Útil para imprimir en orden.
void work(int k) { #pragma omp ordered printf(" %d\n", k); // sale 0, 5, 10, ..., 95 en orden}#pragma omp parallel for ordered schedule(dynamic)for (i = lb; i < ub; i += stride) work(i);flush y el modelo de memoria
Sección titulada «flush y el modelo de memoria»Cada hilo puede tener una vista temporal de las variables (en registros o caché). flush obliga a que esa vista coincida con la memoria. Hay flush implícito en barrier, al entrar y salir de parallel, critical y ordered, y al final de for, sections y single.
int x = 2;#pragma omp parallel num_threads(2) shared(x){ if (omp_get_thread_num() == 0) x = 5; else printf("1: hilo %d: x = %d\n", omp_get_thread_num(), x); // ¿2 o 5? No se sabe #pragma omp barrier // flush implícito: desde aquí todos ven x = 5 if (omp_get_thread_num() == 0) printf("2: hilo %d: x = %d\n", omp_get_thread_num(), x); else printf("3: hilo %d: x = %d\n", omp_get_thread_num(), x);}Print 1 puede mostrar 2 o 5. Print 2 y Print 3 muestran 5 gracias a la barrera.
Ejercicio
Sección titulada «Ejercicio»Compara las tres versiones para el máximo de work(i), i = 0 … 2²⁰−1: (a) critical dentro del for, (b) máximo privado + critical al final, (c) reduction(max:mx). ¿Cuál es más rápida y por qué?
Ver solución
(a) es la más lenta: cada iteración pide el candado, así que hay 2²⁰ entradas en fila. (b) entra a la región crítica solo T veces (una por hilo). (c) hace lo mismo que (b) pero automático: copia privada inicializada con el neutro de max (el menor valor representable) y combinación al final. (b) y (c) son similares y mucho más rápidas que (a); (c) es la más limpia:
#pragma omp parallel for reduction(max:mx)for (int i = 0; i < n; i++) { int r = work(i); mx = (r > mx ? r : mx); }Errores típicos
Sección titulada «Errores típicos»- Usar
atomiccon algo que no es una actualización simple: protege solo la actualización de una variable (x++,x += expr); la expresión de la derecha se evalúa sin protección. - Olvidar que todas las
criticalsin nombre comparten el mismo lock: dos regiones que protegen datos distintos se bloquean entre sí. Ponles nombre:critical(hist). - Usar
criticalpara acumular una suma:reductionhace lo mismo cientos de veces más rápido (0.0002 s frente a 0.056 s en la tabla de arriba). - Escribir
#pragma omp orderedsin la cláusulaordereden elfor.
Práctica extra (reto)
Sección titulada «Práctica extra (reto)»Dos hilos hacen cnt++ 3 veces cada uno, sin protección, empezando en 0. ¿Cuál es el valor final mínimo posible?
Ver solución
2 (no 3). Un intercalado que lo logra:
- A lee 0 y se detiene (lo tiene en su registro).
- B hace 2 incrementos completos: cnt = 2.
- A escribe 1 (pisa el 2).
- B lee 1 para su tercer incremento y se detiene.
- A hace sus 2 incrementos restantes: cnt = 2, luego 3.
- B escribe 2.
Por eso las carreras no solo “pierden un poco”: el resultado puede alejarse mucho del correcto.
Autoevaluación
0/6 correctas · 0 respondidascnt++ sin protección con 4 hilos y 10⁶ iteraciones da…critical con nombres distintos…#pragma omp atomic antes de x[k] += a; y[i] += b; protege…Fuente ·U4.1 Paralelismo compartido-OMP.pdf (láminas 23–28) · U4.2 Paralelismo compartido-OMP.pdf (critical, atomic, ordered) · U4.3 Paralelismo compartido-OMP.pdf (uso de memoria) · semana_9/Ejemp05_critical.cpp · semana_9/sesionb/Ejemp03_critical.cpp · semana_9/sesionb/Ejemp03_atomic_critical.c