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).
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:
- P (proberen, probar): decrementar el contador. Si el contador es 0, bloquear.
- V (verhogen, incrementar): incrementar el contador. Si hay hilos bloqueados, despertar uno.
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_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:
- wait (P, down): decrementar el contador. Si el contador es 0, bloquear hasta que sea > 0.
- post (V, up): incrementar el contador. Si hay hilos bloqueados, despertar uno.
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:
- Valor inicial: el número de permisos disponibles al crear el semáforo.
- wait: consume un permiso (decrementa). Si no hay permisos, bloquea.
- post: libera un permiso (incrementa). Despierta a un hilo bloqueado.
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:
| Aspecto | Mutex | Semáforo |
|---|---|---|
| Propietario | El hilo que bloquea es el que desbloquea. | Cualquier hilo puede hacer post. |
| Valor | Binario (bloqueado/libre). | Entero no negativo. |
| Uso | Proteger secciones críticas. | Contar recursos, señalizar. |
| Reentrancia | El mismo hilo no puede bloquear dos veces (con mutex normal). | El mismo hilo puede hacer wait/post múltiples veces. |
| Propósito | Exclusió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:
- Limitar recursos: un pool de conexiones, un buffer de tamaño N.
- Señalización: un hilo señala a otro que un evento ocurrió.
- Coordinación: hacer que los hilos se esperen y se avisen.
8. Propiedades
| Propiedad | ¿La cumple? | Comentario |
|---|---|---|
| Exclusión mutua | Sí (con valor 1) | Un semáforo binario garantiza exclusión mutua. |
| Progreso | Sí | Si el contador > 0, un hilo que hace wait entra. |
| Espera acotada | Sí (con implementación FIFO) | Los hilos se despiertan en orden FIFO. |
| Flexibilidad | Sí | Puede usarse para contar recursos, señalar y coordinar. |
| Simplicidad | Sí | 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:
#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
| Hilo | Acción | Valor del semáforo |
|---|
11. Resumen
- Un semáforo es un contador de permisos con dos operaciones: wait y post.
- wait decrementa el contador y bloquea si es 0.
- post incrementa el contador y despierta a un hilo bloqueado.
- El semáforo no tiene propietario: cualquier hilo puede hacer post.
- Un semáforo binario (valor 1) es equivalente a un mutex.
- Un semáforo general (valor N) limita el número de hilos concurrentes.
- El semáforo es más general que el mutex: puede contar recursos y señalar.
- Dijkstra inventó el semáforo en 1965.
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
- Implementa el programa de semáforo limitador y observa la salida.
- Usa un semáforo binario como mutex y compara con un mutex real.
- Implementa un semáforo con un contador y una cola de espera.
- Usa sem_trywait para intentar adquirir sin bloquear.
- Usa sem_timedwait para adquirir con timeout.
- Investiga cómo se implementa un semáforo en el núcleo de Linux.