5 - Sincronización con Mutex y Secciones Críticas en Windows

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.

5.1 ¿Qué es un mutex en Windows y para qué se usa?

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.

5.2 Mutex de Windows: 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.

5.3 Secciones críticas: 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.

5.4 Problemas comunes: deadlock y doble bloqueo en Windows

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.

5.5 Secciones críticas anidadas y consideraciones de diseño

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.

5.6 Ejemplo clásico: contador compartido entre hilos

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.

5.7 Aplicación en C (CLion, Windows): Contador global protegido con CRITICAL_SECTION

El 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;
}
Salida de consola mostrando el contador protegido con CRITICAL_SECTION y la diferencia al desactivar el lock

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.

Comparación visual de resultados con y sin protección de lock en el contador global