1. El mismo problema
El problema del productor-consumidor es el mismo que en el tema 29: un productor genera datos, un consumidor los procesa, y un buffer acotado los conecta. La diferencia está en la herramienta de sincronización: semáforos vs. variables de condición.
La solución con semáforos usa tres semáforos (mutex, vacíos, llenos). La solución con variables de condición usa un mutex y dos variables de condición (no vacío, no lleno).
2. La solución con monitor
La solución con variables de condición encapsula el buffer y la sincronización en un monitor. El monitor tiene:
- Datos: el buffer, el frente, el fondo, el tamaño, la capacidad.
- Mutex: para la exclusión mutua.
- Variables de condición:
no_vacio(hay datos para consumir) yno_lleno(hay espacio para producir). - Métodos: poner, quitar.
Las variables de condición se usan para esperar a que se cumpla una condición sobre los datos, sin gastar CPU.
3. La estructura del monitor
typedef struct {
int buffer[CAPACIDAD];
int frente;
int fondo;
int tamano;
pthread_mutex_t mutex;
pthread_cond_t no_vacio; /* hay datos para consumir */
pthread_cond_t no_lleno; /* hay espacio para producir */
} cola_t;
La estructura es similar a la del tema 28 (cola acotada como monitor). La diferencia es que ahora se usa en el contexto del productor-consumidor clásico.
4. El productor
El productor espera a que haya espacio en el buffer, inserta el dato y señala a los consumidores:
void *productor(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < ITEMS_POR_PRODUCTOR; i++) {
int dato = id * 100 + i;
pthread_mutex_lock(&cola.mutex);
while (cola.tamano == CAPACIDAD) {
pthread_cond_wait(&cola.no_lleno, &cola.mutex);
}
/* insertar */
cola.buffer[cola.fondo] = dato;
cola.fondo = (cola.fondo + 1) % CAPACIDAD;
cola.tamano++;
printf("Productor %d: produjo %d\n", id, dato);
pthread_cond_signal(&cola.no_vacio);
pthread_mutex_unlock(&cola.mutex);
}
return NULL;
}
El productor espera en pthread_cond_wait(&no_lleno) si el buffer está lleno. Cuando un consumidor extrae un elemento, hace pthread_cond_signal(&no_lleno), despertando al productor.
5. El consumidor
El consumidor espera a que haya datos en el buffer, extrae el dato y señala a los productores:
void *consumidor(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < ITEMS_POR_CONSUMIDOR; i++) {
pthread_mutex_lock(&cola.mutex);
while (cola.tamano == 0) {
pthread_cond_wait(&cola.no_vacio, &cola.mutex);
}
/* extraer */
int dato = cola.buffer[cola.frente];
cola.frente = (cola.frente + 1) % CAPACIDAD;
cola.tamano--;
printf("Consumidor %d: consumió %d\n", id, dato);
pthread_cond_signal(&cola.no_lleno);
pthread_mutex_unlock(&cola.mutex);
}
return NULL;
}
6. Código completo
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define CAPACIDAD 5
#define NUM_PRODUCTORES 2
#define NUM_CONSUMIDORES 2
#define ITEMS_POR_PRODUCTOR 10
typedef struct {
int buffer[CAPACIDAD];
int frente;
int fondo;
int tamano;
pthread_mutex_t mutex;
pthread_cond_t no_vacio;
pthread_cond_t no_lleno;
} cola_t;
void cola_init(cola_t *c) {
c->frente = 0; c->fondo = 0; c->tamano = 0;
pthread_mutex_init(&c->mutex, NULL);
pthread_cond_init(&c->no_vacio, NULL);
pthread_cond_init(&c->no_lleno, NULL);
}
void *productor(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < ITEMS_POR_PRODUCTOR; i++) {
int dato = id * 100 + i;
pthread_mutex_lock(&cola.mutex);
while (cola.tamano == CAPACIDAD) {
pthread_cond_wait(&cola.no_lleno, &cola.mutex);
}
cola.buffer[cola.fondo] = dato;
cola.fondo = (cola.fondo + 1) % CAPACIDAD;
cola.tamano++;
printf("Productor %d: produjo %d\n", id, dato);
pthread_cond_signal(&cola.no_vacio);
pthread_mutex_unlock(&cola.mutex);
}
return NULL;
}
void *consumidor(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < (NUM_PRODUCTORES * ITEMS_POR_PRODUCTOR) / NUM_CONSUMIDORES; i++) {
pthread_mutex_lock(&cola.mutex);
while (cola.tamano == 0) {
pthread_cond_wait(&cola.no_vacio, &cola.mutex);
}
int dato = cola.buffer[cola.frente];
cola.frente = (cola.frente + 1) % CAPACIDAD;
cola.tamano--;
printf("Consumidor %d: consumió %d\n", id, dato);
pthread_cond_signal(&cola.no_lleno);
pthread_mutex_unlock(&cola.mutex);
}
return NULL;
}
int main(void) {
cola_t cola;
cola_init(&cola);
pthread_t prods[NUM_PRODUCTORES];
pthread_t cons[NUM_CONSUMIDORES];
int ids_prod[NUM_PRODUCTORES];
int ids_cons[NUM_CONSUMIDORES];
for (int i = 0; i < NUM_PRODUCTORES; i++) {
ids_prod[i] = i;
pthread_create(&prods[i], NULL, productor, &ids_prod[i]);
}
for (int i = 0; i < NUM_CONSUMIDORES; i++) {
ids_cons[i] = i;
pthread_create(&cons[i], NULL, consumidor, &ids_cons[i]);
}
for (int i = 0; i < NUM_PRODUCTORES; i++) pthread_join(prods[i], NULL);
for (int i = 0; i < NUM_CONSUMIDORES; i++) pthread_join(cons[i], NULL);
pthread_mutex_destroy(&cola.mutex);
pthread_cond_destroy(&cola.no_vacio);
pthread_cond_destroy(&cola.no_lleno);
return 0;
}
7. Comparación con semáforos
| Aspecto | Semáforos | Variables de condición |
|---|---|---|
| Mecanismo | 3 semáforos (mutex, vacíos, llenos). | 1 mutex + 2 variables de condición. |
| Espera | sem_wait bloquea. | pthread_cond_wait libera mutex y bloquea. |
| Señal | sem_post incrementa contador. | pthread_cond_signal despierta a un hilo. |
| Estado | El estado está en los contadores de los semáforos. | El estado está en los datos compartidos (buffer, tamaño). |
| Flexibilidad | Menos flexible: solo contar y señalar. | Más flexible: cualquier condición sobre los datos. |
| Legibilidad | Menos legible: la lógica está distribuida en los semáforos. | Más legible: la lógica está en el código. |
| Rendimiento | Similar (ambos usan futexes). | Similar (ambos usan futexes). |
8. Ventajas de las variables de condición
- Más expresivas: se puede esperar a cualquier condición sobre los datos, no solo a un contador.
- Más legibles: la lógica de la condición está en el código, no distribuida en semáforos.
- Más flexibles: se pueden añadir nuevas condiciones sin añadir nuevos semáforos.
- Integración con monitores: las variables de condición son la base de los monitores.
9. Desventajas
- Más verboso: se necesita más código que con semáforos.
- Requiere mutex: siempre se usa con un mutex, lo que añade complejidad.
- Despertares espurios: se necesita while en lugar de if.
- Menos intuitivo: el concepto de «condición» es más abstracto que «contador».
10. Cuándo usar cada solución
| Situación | Semáforos | Variables de condición |
|---|---|---|
| Limitar recursos (pool de N) | Mejor (semáforo con valor N). | Posible pero más complejo. |
| Señalización simple | Mejor (semáforo con valor 0). | Posible pero más complejo. |
| Productor-consumidor simple | Bueno (3 semáforos). | Bueno (mutex + 2 cond). |
| Condiciones complejas | Difícil (necesita más semáforos). | Mejor (cualquier condición). |
| Monitores | No aplicable. | Necesario. |
La regla práctica es: si la condición es simple (contar recursos, señalar), usar semáforos. Si la condición es compleja (depende de múltiples variables), usar variables de condición.
11. Simulador interactivo
Este simulador compara las dos soluciones para el productor-consumidor:
Simulador · productor-consumidor (condición vs. semáforo)
| Paso | Hilo | Operación | Buffer | Estado |
|---|
12. Resumen
- El productor-consumidor se puede resolver con semáforos o con variables de condición.
- Los semáforos usan 3 semáforos (mutex, vacíos, llenos).
- Las variables de condición usan 1 mutex + 2 variables de condición.
- Las variables de condición son más expresivas y flexibles.
- Los semáforos son más simples para casos simples.
- La elección depende de la complejidad de la condición.
13. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Cuántos semáforos usa la solución con semáforos?
3: mutex, vacíos, llenos.
2. ¿Cuántos mecanismos usa la solución con variables de condición?
1 mutex + 2 variables de condición.
3. ¿Cuál es más expresiva?
Las variables de condición (pueden esperar a cualquier condición).
4. ¿Cuál es más simple para casos simples?
Los semáforos.
5. ¿Cuál es más legible?
Las variables de condición (la lógica está en el código).
6. ¿Hay diferencia de rendimiento?
No significativa (ambos usan futexes).
7. ¿Cuándo se deben usar semáforos?
Cuando la condición es simple (contar recursos, señalar).
8. ¿Cuándo se deben usar variables de condición?
Cuando la condición es compleja (depende de múltiples variables).
9. ¿Por qué se usa while y no if en la solución con condición?
Por los despertares espurios.
10. ¿Qué ventaja tienen las variables de condición para monitores?
Son la base de los monitores: encapsulan datos y sincronización.
Para practicar
- Implementa ambas soluciones y compara el código.
- Modifica la solución con condición para añadir una prioridad (consumir los elementos más grandes primero).
- Explica por qué los semáforos son menos flexibles.
- Investiga cómo Java implementa el productor-consumidor (BlockingQueue).
- Compara el rendimiento de ambas soluciones.
- Implementa un productor-consumidor con prioridades usando variables de condición.