← Concurrencia y Sincronización Bloque 7 · Problemas clásicos de sincronización · Tema 30 de 47

Tema 30

Productor-consumidor con variables de condición

En el tema anterior resolviste el productor-consumidor con semáforos. Ahora lo resuelves con un monitor (mutex + variables de condición), que es más expresivo y más fácil de extender. En este tema verás la implementación completa, la comparación entre ambas soluciones y cuándo conviene usar cada una.

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:

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

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:

productor con condición
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:

consumidor con condición
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

productor_consumidor_cond.c
#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

AspectoSemáforosVariables de condición
Mecanismo3 semáforos (mutex, vacíos, llenos).1 mutex + 2 variables de condición.
Esperasem_wait bloquea.pthread_cond_wait libera mutex y bloquea.
Señalsem_post incrementa contador.pthread_cond_signal despierta a un hilo.
EstadoEl estado está en los contadores de los semáforos.El estado está en los datos compartidos (buffer, tamaño).
FlexibilidadMenos flexible: solo contar y señalar.Más flexible: cualquier condición sobre los datos.
LegibilidadMenos legible: la lógica está distribuida en los semáforos.Más legible: la lógica está en el código.
RendimientoSimilar (ambos usan futexes).Similar (ambos usan futexes).

8. Ventajas de las variables de condición

9. Desventajas

10. Cuándo usar cada solución

SituaciónSemáforosVariables de condición
Limitar recursos (pool de N)Mejor (semáforo con valor N).Posible pero más complejo.
Señalización simpleMejor (semáforo con valor 0).Posible pero más complejo.
Productor-consumidor simpleBueno (3 semáforos).Bueno (mutex + 2 cond).
Condiciones complejasDifícil (necesita más semáforos).Mejor (cualquier condición).
MonitoresNo 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)

PasoHiloOperaciónBufferEstado

12. Resumen

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

  1. Implementa ambas soluciones y compara el código.
  2. Modifica la solución con condición para añadir una prioridad (consumir los elementos más grandes primero).
  3. Explica por qué los semáforos son menos flexibles.
  4. Investiga cómo Java implementa el productor-consumidor (BlockingQueue).
  5. Compara el rendimiento de ambas soluciones.
  6. Implementa un productor-consumidor con prioridades usando variables de condición.