En este tema vemos cómo sincronizar hilos en Windows usando mutex y secciones críticas. Repasamos APIs clave, riesgos de deadlock y un ejemplo práctico de contador compartido protegido con CRITICAL_SECTION.
Un mutex (mutual exclusion) es un objeto de kernel que garantiza acceso exclusivo a un recurso compartido. Un solo hilo puede poseerlo; los demás esperan hasta que se libere.
CreateMutex(), WaitForSingleObject(), ReleaseMutex()Con CreateMutex se crea/abre un mutex. WaitForSingleObject bloquea hasta adquirirlo o agotar el timeout. ReleaseMutex lo libera. Al ser de kernel, es más pesado que otras alternativas pero sirve entre procesos distintos.
CRITICAL_SECTION, InitializeCriticalSection(), EnterCriticalSection(), LeaveCriticalSection()Una CRITICAL_SECTION es más liviana que un mutex y solo funciona dentro del mismo proceso. Se inicializa una vez, se entra antes de tocar el recurso y se sale al terminar. Es la opción preferida para sincronizar hilos del mismo proceso.
Un deadlock es una espera circular definitiva: cada hilo retiene un recurso y exige otro que ya está tomado, de modo que ninguno puede avanzar sin que otro libere primero. Suele aparecer cuando se adquieren locks en distinto orden o se mezclan recursos de kernel (mutex) y de usuario (secciones críticas) sin una estrategia clara.
Los deadlocks ocurren cuando dos hilos se esperan mutuamente (por ejemplo, cada uno tomó un lock diferente en distinto orden). El doble bloqueo sucede si un hilo intenta entrar dos veces a un mutex no recursivo. Las secciones críticas por defecto son recursivas en Windows, pero abusar de esto puede ocultar diseños deficientes.
Al anidar secciones críticas, define un orden global de adquisición para evitar deadlocks. Mantén los bloqueos lo más breves posible y evita hacer I/O dentro de una sección crítica.
Un contador global es un recurso compartido simple. Sin protección, dos hilos pueden pisar sus valores y producir resultados incorrectos. Con una sección crítica, cada incremento se vuelve atómico respecto a otros hilos.
CRITICAL_SECTIONEl programa declara un contador global y crea varios hilos que lo incrementan muchas veces. Puedes habilitar o deshabilitar el lock con #define USE_LOCK para ver la diferencia entre ejecución segura y con condición de carrera. Basta con comentar o descomentar esa línea y recompilar para alternar entre la versión protegida y la que deja expuesta la sección crítica.
#include <windows.h>
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
#define HILOS 8
#define ITERACIONES 500000
/* Comenta o descomenta para activar proteccion */
#define USE_LOCK
static volatile long long contador = 0;
#ifdef USE_LOCK
static CRITICAL_SECTION cs;
#endif
unsigned __stdcall incrementar(void *arg) {
(void)arg;
for (int i = 0; i < ITERACIONES; i++) {
#ifdef USE_LOCK
EnterCriticalSection(&cs);
#endif
contador++;
#ifdef USE_LOCK
LeaveCriticalSection(&cs);
#endif
}
return 0;
}
int main(void) {
#ifdef USE_LOCK
InitializeCriticalSection(&cs);
#endif
HANDLE handles[HILOS] = {0};
for (int i = 0; i < HILOS; i++) {
uintptr_t h = _beginthreadex(
NULL,
0,
incrementar,
NULL,
0,
NULL
);
if (h == 0) {
fprintf(stderr, "No se pudo crear el hilo %d\n", i);
return EXIT_FAILURE;
}
handles[i] = (HANDLE)h;
}
WaitForMultipleObjects(HILOS, handles, TRUE, INFINITE);
for (int i = 0; i < HILOS; i++) {
CloseHandle(handles[i]);
}
printf("Valor final del contador: %lld\n", contador);
#ifdef USE_LOCK
DeleteCriticalSection(&cs);
#endif
return EXIT_SUCCESS;
}
En ejecución con USE_LOCK el resultado debería ser HILOS * ITERACIONES. Si desactivas el lock, observarás códigos menores por condiciones de carrera. Experimenta con más hilos o iteraciones para amplificar el efecto.