← Concurrencia y Sincronización Bloque 5 · Semáforos · Tema 20 de 47

Tema 20

Semáforos: wait y post

El semáforo es la abstracción de sincronización más general: un contador que cuenta permisos. Un hilo espera (wait) hasta que haya un permiso disponible, y otro hilo señala (post) que ha liberado un permiso. En este tema verás la abstracción de Dijkstra, cómo funciona el contador, y la diferencia fundamental con un mutex.

1. Qué es un semáforo

Un semáforo es una variable entera (no negativa) que se manipula con dos operaciones atómicas: wait (también llamada P o down) y post (también llamada V o up). El semáforo cuenta permisos: wait decrementa el contador y bloquea si es cero; post incrementa el contador y despierta a un hilo bloqueado.

El semáforo fue inventado por Edsger Dijkstra en 1965. Es la abstracción de sincronización más general: con semáforos se pueden implementar mutex, variables de condición, barreras, y resolver cualquier problema de sincronización.

La idea es simple: un semáforo con valor N permite que N hilos entren a la sección crítica simultáneamente. Un semáforo con valor 1 es un mutex. Un semáforo con valor 0 es una señal (un hilo espera a que otro lo señale).

El semáforo es un contador de permisos

Un semáforo con valor 3 permite que 3 hilos pasen. Cada wait consume un permiso; cada post libera uno. Si no hay permisos (valor 0), wait bloquea. Es como un estacionamiento con N plazas: wait es entrar, post es salir.

2. La abstracción de Dijkstra

Dijkstra definió el semáforo como una abstracción con dos operaciones primitivas:

Las operaciones P y V son atómicas: no se pueden interrumpir. El semáforo garantiza que el contador nunca es negativo.

Dijkstra demostró que con semáforos se puede resolver cualquier problema de sincronización. Es la herramienta universal de la concurrencia.

3. wait y post

En POSIX (y en la mayoría de las bibliotecas), las operaciones se llaman wait (o down) y post (o up):

semáforo básico
sem_t sem;
sem_init(&sem, 0, 3);  /* valor inicial = 3 */

/* Hilo 1: consumir un permiso. */
sem_wait(&sem);  /* decrementa a 2, no bloquea */
/* ... usar el recurso ... */
sem_post(&sem);  /* incrementa a 3 */

/* Hilo 2: consumir un permiso. */
sem_wait(&sem);  /* decrementa a 2 */
/* ... usar el recurso ... */
sem_post(&sem);  /* incrementa a 3 */

La semántica es:

Las operaciones son atómicas: no se pueden interrumpir. El contador nunca es negativo.

4. El contador que cuenta permisos

El semáforo mantiene un contador interno que representa el número de permisos disponibles:

El contador puede ser mayor que 1 (semáforo general) o igual a 1 (semáforo binario, equivalente a un mutex).

La implementación típica mantiene una cola de hilos bloqueados. Cuando se hace post, se despierta al primer hilo de la cola (FIFO).

5. Semáforo vs. mutex

La diferencia fundamental entre un semáforo y un mutex es:

AspectoMutexSemáforo
PropietarioEl hilo que bloquea es el que desbloquea.Cualquier hilo puede hacer post.
ValorBinario (bloqueado/libre).Entero no negativo.
UsoProteger secciones críticas.Contar recursos, señalizar.
ReentranciaEl mismo hilo no puede bloquear dos veces (con mutex normal).El mismo hilo puede hacer wait/post múltiples veces.
PropósitoExclusión mutua.Sincronización general.

La diferencia clave es el propietario: un mutex tiene propietario (el hilo que lo bloqueó debe desbloquearlo), mientras que un semáforo no tiene propietario (cualquier hilo puede hacer post).

Esto significa que un semáforo puede usarse para señalizar entre hilos: un hilo hace post para señalar que ocurrió un evento, y otro hilo hace wait para esperar el evento.

6. Semáforo binario

Un semáforo binario tiene valor inicial 1 y se usa como un mutex. La diferencia es que no tiene propietario: cualquier hilo puede hacer post.

El semáforo binario es equivalente a un mutex en términos de exclusión mutua, pero con la flexibilidad de que cualquier hilo puede liberarlo. Esto puede ser útil en algunos casos (por ejemplo, cuando un hilo produce un recurso y otro lo consume), pero también puede causar errores si se usa incorrectamente.

7. Semáforo general

Un semáforo general tiene valor inicial N (mayor que 1) y se usa para limitar el número de hilos que pueden acceder a un recurso simultáneamente. Por ejemplo, un semáforo con valor 3 permite que hasta 3 hilos accedan a un recurso a la vez.

El semáforo general es útil para:

8. Propiedades

Propiedad¿La cumple?Comentario
Exclusión mutuaSí (con valor 1)Un semáforo binario garantiza exclusión mutua.
ProgresoSíSi el contador > 0, un hilo que hace wait entra.
Espera acotadaSí (con implementación FIFO)Los hilos se despiertan en orden FIFO.
FlexibilidadSíPuede usarse para contar recursos, señalar y coordinar.
SimplicidadSíSolo dos operaciones: wait y post.

9. Un ejemplo desarrollado

Un ejemplo de uso de semáforo para limitar el número de hilos que acceden a un recurso:

semaforo_limite.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define NUM_HILOS 10
#define MAX_CONCURRENTES 3

sem_t sem;

void *trabajar(void *arg) {
    int id = *(int *)arg;
    printf("Hilo %d: esperando...\n", id);
    sem_wait(&sem);  /* consumir un permiso */
    printf("Hilo %d: entrando (permisos disponibles: ?)\n", id);
    sleep(1);  /* simular trabajo */
    printf("Hilo %d: saliendo\n", id);
    sem_post(&sem);  /* liberar un permiso */
    return NULL;
}

int main(void) {
    pthread_t hilos[NUM_HILOS];
    int ids[NUM_HILOS];
    sem_init(&sem, 0, MAX_CONCURRENTES);

    for (int i = 0; i < NUM_HILOS; i++) {
        ids[i] = i;
        pthread_create(&hilos[i], NULL, trabajar, &ids[i]);
    }
    for (int i = 0; i < NUM_HILOS; i++) {
        pthread_join(hilos[i], NULL);
    }
    sem_destroy(&sem);
    return 0;
}

Con MAX_CONCURRENTES = 3, solo 3 hilos pueden estar en la sección crítica simultáneamente. Los demás esperan. Cuando uno sale (post), otro entra (wait).

10. Simulador interactivo

Este simulador muestra cómo funciona un semáforo con varios hilos:

Simulador · semáforo

HiloAcciónValor del semáforo

11. Resumen

12. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es un semáforo?

Un contador de permisos con dos operaciones atómicas: wait y post.

2. ¿Qué hace wait?

Decrementa el contador. Si es 0, bloquea hasta que sea > 0.

3. ¿Qué hace post?

Incrementa el contador. Si hay hilos bloqueados, despierta uno.

4. ¿Cuál es la diferencia principal entre semáforo y mutex?

El mutex tiene propietario (el que bloquea desbloquea); el semáforo no (cualquier hilo puede hacer post).

5. ¿Qué es un semáforo binario?

Un semáforo con valor inicial 1, equivalente a un mutex.

6. ¿Para qué se usa un semáforo general?

Para limitar el número de hilos que acceden a un recurso simultáneamente.

7. ¿Quién inventó el semáforo?

Edsger Dijkstra, en 1965.

8. ¿Qué operaciones son atómicas en un semáforo?

wait y post.

9. ¿Puede el contador de un semáforo ser negativo?

No. El semáforo garantiza que el contador nunca es negativo.

10. ¿Qué orden de despertar se usa típicamente?

FIFO: el primer hilo en bloquearse es el primero en despertarse.

Para practicar

  1. Implementa el programa de semáforo limitador y observa la salida.
  2. Usa un semáforo binario como mutex y compara con un mutex real.
  3. Implementa un semáforo con un contador y una cola de espera.
  4. Usa sem_trywait para intentar adquirir sin bloquear.
  5. Usa sem_timedwait para adquirir con timeout.
  6. Investiga cómo se implementa un semáforo en el núcleo de Linux.