Fundamentos

Riesgos del estado compartido

Dos hilos que tocan la misma variable sin coordinarse producen resultados que dependen del azar del planificador. En una planta eso significa contadores de piezas que no cuadran, motores que se activan dos veces o robots que colisionan. Estos son los fallos que hay que conocer antes de escribir la primera línea de sincronización.

Condición de carrera

Dos hilos de una línea de envasado comparten el contador de cajas que faltan por llenar:

int cajasPendientes = 0;   // COMPARTIDO por dos hilos

void hCintaA() {
    // ... llega una caja ...
    cajasPendientes = cajasPendientes + 1;   // ← no atómico
}

void hCintaB() {
    // ... llega otra caja ...
    cajasPendientes = cajasPendientes + 1;   // ← no atómico
}

x = x + 1 son en realidad tres pasos: leer el valor, sumar y escribir. Si ambos hilos leen el mismo valor antes de que ninguno escriba, una de las cajas se pierde:

InstantehCintaAhCintaBcajasPendientes
1lee 00
2lee 00
3escribe 0+1 = 11
4escribe 0+1 = 11 ← ¡deberían ser 2!

El resultado correcto era 2; el real, 1. El fallo depende del interleaving (entrelazado) de las instrucciones y por eso es tan difícil de reproducir: «en las pruebas siempre funcionaba».

📘 Definición

Una región crítica es un tramo de código que accede a datos compartidos y que debe ejecutarse sin que otro hilo entre en él. Todo el arte de la sincronización consiste en proteger regiones críticas sin ahogar el paralelismo.

Atomicidad

Una operación es atómica cuando se ejecuta como un todo indivisible: ningún hilo puede observar ni interrumpir un estado intermedio. x++ no lo es en la mayoría de arquitecturas; las operaciones sobre std::atomic<int> sí lo son (se estudian en Atómicos). La otra forma de conseguir atomicidad es encerrar la región crítica con un mutex.

Data race: comportamiento indefinido

Cuando dos hilos acceden a la misma posición de memoria, al menos uno escribe, y no hay sincronización entre ellos, el estándar lo llama data race y el programa tiene comportamiento indefinido: puede dar valores erróneos, corromper estructuras o fallar solo en producción. Detectarlos a ojo es imposible; para eso existen las herramientas:

# Compila con información de depuración y ThreadSanitizer
g++ -std=c++20 -g -fsanitize=thread planta.cpp -o planta -pthread
./planta        # el informe de carreras aparece en la consola

Interbloqueo (deadlock)

Dos hilos se esperan mutuamente y ninguno puede avanzar. El ejemplo clásico con dos recursos (la prensa y el robot):

Reglas para evitarlo:

  1. Orden fijo de adquisición: todos los hilos toman los candados en el mismo orden.
  2. std::scoped_lock: toma varios mutex de una vez con algoritmo anti-deadlock.
  3. No mantener candados mientras se espera por otro recurso bloqueante.
  4. Usar esperas con timeout (try_acquire_for, wait_for) para detectar el problema.
💡 Pruébalo

En el laboratorio de la célula FMS el robot es región crítica: observa cómo los cuatro hilos piden sRobot y cómo el semáforo los turna sin colisiones.

Inanición y prioridad inversa

Resumen de amenazas

AmenazaSíntomaSolución
Condición de carreraResultados que dependen del orden de ejecuciónMutex, semáforos, atómicos
Data raceComportamiento indefinido, corrupciónSincronización + ThreadSanitizer
DeadlockLa instalación se congelaOrden fijo, scoped_lock, timeouts
InaniciónUna estación nunca trabajaColas FIFO, semáforos con turno
Busy-waitCPU al 100 % sin producirBloqueos reales (semáforos, cond. variables)