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.
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.
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.
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.
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.
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.
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.