6 - Semáforos en Windows

Profundizamos en los semáforos de Windows, sus diferencias con mutex, y cómo combinarlos con secciones críticas para resolver el patrón productor-consumidor. Incluimos un ejemplo en C que usa semáforos de conteo y un buffer circular protegido.

6.1 ¿Qué es un semáforo en el modelo de Windows?

Un semáforo es un objeto de kernel con un contador interno. Controla la cantidad de hilos que pueden acceder simultáneamente a un recurso o sección. El contador disminuye al esperar y aumenta al liberar; cuando llega a cero, los hilos que llaman a WaitForSingleObject quedan bloqueados hasta que otro hilo lo incremente.

6.2 Creación y uso: CreateSemaphore(), WaitForSingleObject(), ReleaseSemaphore(), CloseHandle()

CreateSemaphore crea o abre un semáforo con un valor inicial y un máximo. WaitForSingleObject resta 1 del contador si es mayor que cero (entrada inmediata) o bloquea hasta que lo sea. ReleaseSemaphore suma al contador y despierta hilos en espera. Al terminar, debe cerrarse con CloseHandle para liberar recursos de kernel.

6.3 Semáforos binarios vs semáforos contadores en Windows

Un semáforo binario oscila entre 0 y 1 y se usa como un flag de disponibilidad (similar a un mutex simple, pero sin dueño). Un semáforo de conteo permite valores mayores que 1 y representa cupos disponibles, útil para limitar concurrencia o modelar espacios libres en un buffer.

6.4 Coordinación productor-consumidor con semáforos y mutex/sección crítica

En el patrón productor-consumidor, un semáforo de “vacíos” indica cuántos slots quedan libres y otro de “llenos” indica cuántos ítems hay listos. Un mutex o CRITICAL_SECTION protege la estructura del buffer (índices y datos) mientras se inserta o extrae.

6.5 Diferencias conceptuales entre semáforos y mutex en Windows

El mutex tiene dueño: solo el hilo que lo adquiere puede liberarlo y está pensado para exclusión mutua. El semáforo no tiene dueño; cualquier hilo puede liberar. El mutex suele proteger una sección crítica; el semáforo de conteo representa capacidad disponible. Usar semáforos como “locks” puede ocultar errores de propiedad y reentrancia.

6.6 Aplicación en C (CLion, Windows): Productor-consumidor con semáforos de Windows

El programa implementa un buffer circular compartido. Un hilo productor inserta números consecutivos; un hilo consumidor los retira y muestra. Usa dos semáforos: uno para ítems llenos y otro para espacios vacíos. Una CRITICAL_SECTION protege el acceso al buffer. Corre hasta que el usuario presiona una tecla; luego libera los semáforos para despertar hilos y cerrar limpiamente.

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

#define BUFFER_SIZE 8

static int buffer[BUFFER_SIZE];
static int head = 0; /* próxima posición de escritura */
static int tail = 0; /* próxima posición de lectura */
static volatile LONG detener = 0;
static CRITICAL_SECTION cs;
static HANDLE semVacios = NULL;
static HANDLE semLlenos = NULL;

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

unsigned __stdcall productor(void *arg) {
  (void)arg;
  int valor = 1;
  while (seguir_ejecutando()) {
    DWORD espera = WaitForSingleObject(semVacios, 100);
    if (espera == WAIT_TIMEOUT) {
      continue;
    }
    if (!seguir_ejecutando()) {
      break;
    }

    EnterCriticalSection(&cs);
    buffer[head] = valor++;
    head = (head + 1) % BUFFER_SIZE;
    LeaveCriticalSection(&cs);

    ReleaseSemaphore(semLlenos, 1, NULL);
    Sleep(50);
  }
  return 0;
}

unsigned __stdcall consumidor(void *arg) {
  (void)arg;
  while (seguir_ejecutando()) {
    DWORD espera = WaitForSingleObject(semLlenos, 100);
    if (espera == WAIT_TIMEOUT) {
      continue;
    }
    if (!seguir_ejecutando()) {
      break;
    }

    EnterCriticalSection(&cs);
    int dato = buffer[tail];
    tail = (tail + 1) % BUFFER_SIZE;
    LeaveCriticalSection(&cs);

    printf("Consumidor obtuvo: %d\n", dato);
    ReleaseSemaphore(semVacios, 1, NULL);
    Sleep(80);
  }
  return 0;
}

int main(void) {
  InitializeCriticalSection(&cs);
  semVacios = CreateSemaphore(NULL, BUFFER_SIZE, BUFFER_SIZE, NULL);
  semLlenos = CreateSemaphore(NULL, 0, BUFFER_SIZE, NULL);
  if (!semVacios || !semLlenos) {
    fprintf(stderr, "No se pudieron crear los semaforos (%lu)\n", GetLastError());
    return EXIT_FAILURE;
  }

  HANDLE hilos[2] = {0};
  uintptr_t hProd = _beginthreadex(NULL, 0, productor, NULL, 0, NULL);
  uintptr_t hCons = _beginthreadex(NULL, 0, consumidor, NULL, 0, NULL);
  if (hProd == 0 || hCons == 0) {
    fprintf(stderr, "No se pudieron crear los hilos\n");
    InterlockedExchange(&detener, 1);
  } else {
    hilos[0] = (HANDLE)hProd;
    hilos[1] = (HANDLE)hCons;
    puts("Productor-consumidor en marcha. Presiona cualquier tecla para salir...");
    while (!_kbhit()) {
      Sleep(100);
    }
    _getch(); /* consume la tecla */
    InterlockedExchange(&detener, 1);
    ReleaseSemaphore(semVacios, BUFFER_SIZE, NULL);
    ReleaseSemaphore(semLlenos, BUFFER_SIZE, NULL);
    WaitForMultipleObjects(2, hilos, TRUE, INFINITE);
  }

  if (hilos[0]) CloseHandle(hilos[0]);
  if (hilos[1]) CloseHandle(hilos[1]);
  if (semVacios) CloseHandle(semVacios);
  if (semLlenos) CloseHandle(semLlenos);
  DeleteCriticalSection(&cs);
  return EXIT_SUCCESS;
}

Puedes ajustar BUFFER_SIZE o las llamadas a Sleep para ver cómo se alteran los tiempos de espera y el interleaving. Si quieres simular más carga, duplica los consumidores con el mismo semáforo de llenos o agrega productores para saturar el buffer.