Repasamos fallas frecuentes al programar con hilos en Windows: condiciones de carrera, inconsistencias por falta de sincronización, deadlocks, livelock y starvation. Cerramos con un demostrador en C que exhibe tanto el comportamiento erróneo como la versión corregida.
Ocurren cuando dos o más hilos acceden a datos compartidos sin coordinación y al menos uno escribe. El resultado depende del orden de ejecución y puede variar en cada corrida.
Se detectan al ver valores finales erráticos, lecturas de datos sin inicializar o crasheos intermitentes. El remedio habitual es envolver la región crítica con CRITICAL_SECTION o usar tipos atómicos (Interlocked*) cuando el acceso es simple.
Incluso si no hay crash, leer datos parcialmente actualizados genera estados incoherentes (por ejemplo, estructura con campos mezclados). Las secciones críticas garantizan que las lecturas y escrituras sean atómicas a nivel lógico.
El patrón: productor escribe varios campos y consumidor lee mientras se está escribiendo. Solución: proteger ambos lados o usar un flag atómico que indique “dato listo” tras la escritura completa.
Dos hilos toman locks en distinto orden y se esperan mutuamente. Ninguno puede avanzar, consumiendo recursos o bloqueando toda la aplicación.
Prevención: definir un orden global de adquisición, evitar locks innecesarios y fijar timeouts en operaciones de bloqueo para detectar el problema en pruebas (WaitForSingleObject con tiempo finito + log).
En un livelock los hilos siguen ejecutando pero cediendo continuamente sin progresar. La starvation aparece cuando un hilo nunca obtiene CPU o locks porque otros acaparan prioridad o recursos.
Ejemplo de livelock: dos hilos detectan contención y liberan/reintentan en bucle rápido sin avanzar. Ejemplo de starvation: un hilo de baja prioridad nunca logra ejecutar porque siempre hay hilos de prioridad alta listos. Estrategias: backoff exponencial, prioridades balanceadas y uso de SwitchToThread/Sleep con pausas prudentes.
volatile o memoria protegida), generando lecturas obsoletas.El programa puede ejecutarse en modo incorrecto (race y posible deadlock) o en modo corregido (orden de locks coherente y protección de contadores). Usa #define MODO_INSEGURO para alternar.
En modo inseguro se fuerza una ventana de intercalado con trabajo artificial, se adquieren locks en orden opuesto y el contador no está protegido si no se alcanzan ambos locks. En modo seguro se unifica el orden y se elimina la posibilidad de carrera y deadlock.
#include <windows.h>
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
#define ITERACIONES 200000
#define HILOS 2
/* Comenta para ver la versión corregida (sin deadlock y sin race) */
#define MODO_INSEGURO
static volatile long contador = 0;
static CRITICAL_SECTION csA;
static CRITICAL_SECTION csB;
static void trabajo_ligero(void) {
for (volatile int i = 0; i < 1000; i++) {
/* bucle vacío para dar tiempo a interleaving */
}
}
unsigned __stdcall hilo_a(void *arg) {
(void)arg;
for (int i = 0; i < ITERACIONES; i++) {
#ifdef MODO_INSEGURO
/* Lock en orden A luego B: puede deadlock con hilo_b */
EnterCriticalSection(&csA);
trabajo_ligero();
EnterCriticalSection(&csB);
contador++; /* race si no se llega a tomar ambos locks */
LeaveCriticalSection(&csB);
LeaveCriticalSection(&csA);
#else
/* Orden consistente: primero B, luego A para ambos hilos */
EnterCriticalSection(&csB);
EnterCriticalSection(&csA);
contador++;
LeaveCriticalSection(&csA);
LeaveCriticalSection(&csB);
#endif
}
return 0;
}
unsigned __stdcall hilo_b(void *arg) {
(void)arg;
for (int i = 0; i < ITERACIONES; i++) {
#ifdef MODO_INSEGURO
/* Lock en orden B luego A: inverso al hilo_a, posible deadlock */
EnterCriticalSection(&csB);
trabajo_ligero();
EnterCriticalSection(&csA);
contador++;
LeaveCriticalSection(&csA);
LeaveCriticalSection(&csB);
#else
EnterCriticalSection(&csB);
EnterCriticalSection(&csA);
contador++;
LeaveCriticalSection(&csA);
LeaveCriticalSection(&csB);
#endif
}
return 0;
}
int main(void) {
InitializeCriticalSection(&csA);
InitializeCriticalSection(&csB);
HANDLE hilos[HILOS] = {0};
uintptr_t h1 = _beginthreadex(NULL, 0, hilo_a, NULL, 0, NULL);
uintptr_t h2 = _beginthreadex(NULL, 0, hilo_b, NULL, 0, NULL);
if (h1 == 0 || h2 == 0) {
fprintf(stderr, "No se pudieron crear los hilos\n");
return EXIT_FAILURE;
}
hilos[0] = (HANDLE)h1;
hilos[1] = (HANDLE)h2;
DWORD espera = WaitForMultipleObjects(HILOS, hilos, TRUE, 5000);
if (espera == WAIT_TIMEOUT) {
puts("Probable deadlock en modo inseguro (timeout alcanzado).");
} else {
printf("Contador final: %ld (esperado %d)\n", contador, ITERACIONES * HILOS);
}
for (int i = 0; i < HILOS; i++) {
CloseHandle(hilos[i]);
}
DeleteCriticalSection(&csA);
DeleteCriticalSection(&csB);
return EXIT_SUCCESS;
}
En modo inseguro podrás ver timeouts (deadlock) o valores de contador menores a ITERACIONES * HILOS (race). Al comentar MODO_INSEGURO, el resultado se estabiliza y el programa finaliza sin bloqueos.