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:
| Instante | hCintaA | hCintaB | cajasPendientes |
|---|---|---|---|
| 1 | lee 0 | 0 | |
| 2 | lee 0 | 0 | |
| 3 | escribe 0+1 = 1 | 1 | |
| 4 | escribe 0+1 = 1 | 1 ← ¡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».
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:
- ThreadSanitizer (
-fsanitize=threaden GCC/Clang): avisa de data races en tiempo de ejecución. - Helgrind / DRD (Valgrind): detecta carreras y mal uso de candados.
- Analizadores estáticos (clang-tidy, Visual Studio): encuentran patrones sospechosos sin ejecutar.
# 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):
- El hilo de la prensa toma el mutex de la prensa y pide el del robot.
- El hilo de entrada toma el mutex del robot y pide el de la prensa.
- Cada uno tiene lo que el otro necesita: bloqueo permanente.
Reglas para evitarlo:
- Orden fijo de adquisición: todos los hilos toman los candados en el mismo orden.
- std::scoped_lock: toma varios mutex de una vez con algoritmo anti-deadlock.
- No mantener candados mientras se espera por otro recurso bloqueante.
- Usar esperas con timeout (
try_acquire_for,wait_for) para detectar el problema.
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
- Inanición (starvation): un hilo nunca consigue el recurso porque otros lo acaparan. Se evita con colas de espera justas (FIFO) o turnos.
- Inversión de prioridad: un hilo de baja prioridad retiene un mutex que necesita uno de alta; en tiempo real se mitiga con herencia de prioridad o protocolos de acceso.
Resumen de amenazas
| Amenaza | Síntoma | Solución |
|---|---|---|
| Condición de carrera | Resultados que dependen del orden de ejecución | Mutex, semáforos, atómicos |
| Data race | Comportamiento indefinido, corrupción | Sincronización + ThreadSanitizer |
| Deadlock | La instalación se congela | Orden fijo, scoped_lock, timeouts |
| Inanición | Una estación nunca trabaja | Colas FIFO, semáforos con turno |
| Busy-wait | CPU al 100 % sin producir | Bloqueos reales (semáforos, cond. variables) |