7 - Variables de Condición en Windows

Las variables de condición permiten que un hilo duerma hasta que se cumpla un evento (por ejemplo, que la cola ya no esté vacía). Windows provee CONDITION_VARIABLE para usarse junto con CRITICAL_SECTION o SRWLOCK, logrando esperas y desbloqueos atómicos sin ocupar CPU.

7.1 ¿Qué es una variable de condición en Windows?

Es un objeto ligero de usuario que permite que un hilo espere a que otro lo despierte cuando una condición lógica se cumpla. No almacena estado de la condición; solo coordina las esperas y despertares.

7.2 CONDITION_VARIABLE e interacción con CRITICAL_SECTION

Se inicializa con InitializeConditionVariable. Para usarla, se protege el recurso con una CRITICAL_SECTION; el hilo libera el lock y duerme de manera atómica mediante SleepConditionVariableCS. Al despertar, vuelve a poseer la sección crítica.

7.3 SleepConditionVariableCS(): espera y desbloqueo atómico

Esta función toma un puntero a la variable de condición, la sección crítica y un timeout. Su operación es atómica: suelta el lock, duerme y lo vuelve a adquirir al regresar. Si expira el timeout devuelve FALSE con ERROR_TIMEOUT.

7.4 WakeConditionVariable() y WakeAllConditionVariable()

WakeConditionVariable despierta a un solo hilo en espera. WakeAllConditionVariable despierta a todos los hilos bloqueados en esa variable. No reordena la cola de espera; los hilos contendiendo deben volver a chequear la condición protegida.

7.5 Ejemplo clásico: buffer limitado usando condición y CRITICAL_SECTION

En un buffer acotado, los productores esperan si el buffer está lleno y los consumidores esperan si está vacío. Las variables de condición despiertan al rol opuesto cuando cambian los contadores, evitando busy-waiting y protegiendo la estructura con un lock.

7.6 Aplicación en C (CLion, Windows): Cola de tareas con condition variables

El programa mantiene una cola circular de tareas. Varios productores encolan y varios consumidores desencolan. Los consumidores duermen con SleepConditionVariableCS si no hay datos; los productores los despiertan con WakeConditionVariable (o todos con WakeAllConditionVariable al finalizar). Toda la cola se protege con CRITICAL_SECTION.

#include <windows.h>
#include <process.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_TAREAS 16
#define PRODUCTORES 2
#define CONSUMIDORES 2

typedef struct {
  int id;
  int valor;
} Tarea;

static Tarea cola[MAX_TAREAS];
static int head = 0;
static int tail = 0;
static int count = 0;
static CRITICAL_SECTION cs;
static CONDITION_VARIABLE cvNoVacia;
static CONDITION_VARIABLE cvNoLlena;
static volatile LONG detener = 0;

static int seguir(void) {
  return InterlockedCompareExchange(&detener, 0, 0) == 0;
}

static void encolar(Tarea t) {
  cola[tail] = t;
  tail = (tail + 1) % MAX_TAREAS;
  count++;
}

static Tarea desencolar(void) {
  Tarea t = cola[head];
  head = (head + 1) % MAX_TAREAS;
  count--;
  return t;
}

unsigned __stdcall productor(void *arg) {
  int id = (int)(intptr_t)arg;
  int secuencia = id * 1000;
  while (seguir()) {
    EnterCriticalSection(&cs);
    while (count == MAX_TAREAS && seguir()) {
      SleepConditionVariableCS(&cvNoLlena, &cs, INFINITE);
    }
    if (!seguir()) {
      LeaveCriticalSection(&cs);
      break;
    }
    Tarea t = { .id = id, .valor = ++secuencia };
    encolar(t);
    WakeConditionVariable(&cvNoVacia);
    LeaveCriticalSection(&cs);
    Sleep(80);
  }
  return 0;
}

unsigned __stdcall consumidor(void *arg) {
  int id = (int)(intptr_t)arg;
  while (seguir()) {
    EnterCriticalSection(&cs);
    while (count == 0 && seguir()) {
      SleepConditionVariableCS(&cvNoVacia, &cs, INFINITE);
    }
    if (count == 0 && !seguir()) {
      LeaveCriticalSection(&cs);
      break;
    }
    Tarea t = desencolar();
    WakeConditionVariable(&cvNoLlena);
    LeaveCriticalSection(&cs);

    printf("[consumidor %d] proceso tarea %d (prod %d)\n", id, t.valor, t.id);
    Sleep(120);
  }
  return 0;
}

int main(void) {
  InitializeCriticalSection(&cs);
  InitializeConditionVariable(&cvNoVacia);
  InitializeConditionVariable(&cvNoLlena);

  HANDLE hilos[PRODUCTORES + CONSUMIDORES] = {0};

  for (int i = 0; i < PRODUCTORES; i++) {
    uintptr_t h = _beginthreadex(NULL, 0, productor, (void *)(intptr_t)i, 0, NULL);
    hilos[i] = (HANDLE)h;
  }
  for (int i = 0; i < CONSUMIDORES; i++) {
    uintptr_t h = _beginthreadex(NULL, 0, consumidor, (void *)(intptr_t)i, 0, NULL);
    hilos[PRODUCTORES + i] = (HANDLE)h;
  }

  puts("Cola en marcha. Presiona una tecla para salir...");
  while (!_kbhit()) {
    Sleep(100);
  }
  _getch();
  InterlockedExchange(&detener, 1);

  EnterCriticalSection(&cs);
  WakeAllConditionVariable(&cvNoVacia);
  WakeAllConditionVariable(&cvNoLlena);
  LeaveCriticalSection(&cs);

  WaitForMultipleObjects(PRODUCTORES + CONSUMIDORES, hilos, TRUE, INFINITE);
  for (int i = 0; i < PRODUCTORES + CONSUMIDORES; i++) {
    CloseHandle(hilos[i]);
  }
  DeleteCriticalSection(&cs);
  return EXIT_SUCCESS;
}

Puedes jugar con MAX_TAREAS, la cantidad de productores/consumidores o los Sleep para ver cómo varía el interleaving. Cambia WakeConditionVariable por WakeAllConditionVariable en el productor si quieres despertar a todos los consumidores ante cada inserción.