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

Tema 29

Productor-consumidor con semáforos

El problema del productor-consumidor es el clásico de la sincronización: un productor genera datos y los coloca en un buffer acotado, y un consumidor los retira y procesa. Si el buffer está lleno, el productor espera; si está vacío, el consumidor espera. En este tema verás cómo resolverlo con tres semáforos y por qué el orden de los wait es crítico para evitar el deadlock.

1. El problema

El problema del productor-consumidor (también llamado problema del búfer acotado) consiste en coordinar dos tipos de procesos: los productores que generan datos y los consumidores que los procesan. Los datos se almacenan en un buffer acotado (de capacidad fija). El productor espera si el buffer está lleno; el consumidor espera si está vacío.

El problema tiene dos desafíos:

El problema fue propuesto por Dijkstra en 1965 y es uno de los problemas clásicos de sincronización.

2. El buffer acotado

El buffer acotado es un array circular de capacidad fija. Los productores insertan al final; los consumidores extraen del principio.

buffer acotado
#define CAPACIDAD 10

typedef struct {
    int buffer[CAPACIDAD];
    int frente;
    int fondo;
    int tamano;
} buffer_t;

El buffer es circular: cuando se llega al final, se vuelve al principio. El tamaño indica cuántos elementos hay actualmente.

3. Los tres semáforos

La solución con semáforos usa tres semáforos:

SemáforoValor inicialPropósito
mutex1Exclusión mutua para acceder al buffer.
vaciosCAPACIDADCuenta los espacios vacíos en el buffer.
llenos0Cuenta los elementos en el buffer.

Los semáforos vacios y llenos son la clave: vacios empieza en la capacidad (todos los espacios vacíos) y llenos empieza en 0 (ningún elemento). A medida que se producen y consumen, los contadores se ajustan.

La relación invariante es: vacios + llenos = CAPACIDAD. Esta invariante se mantiene siempre.

4. El productor

El productor genera un dato, espera a que haya un espacio vacío, adquiere el mutex, inserta el dato, libera el mutex y señala que hay un elemento más:

productor
void *productor(void *arg) {
    for (int i = 0; i < N; i++) {
        int dato = producir();  /* generar dato */

        sem_wait(&vacios);       /* esperar espacio vacío */
        pthread_mutex_lock(&mutex);
        insertar(buffer, dato);  /* insertar en el buffer */
        pthread_mutex_unlock(&mutex);
        sem_post(&llenos);       /* señalar que hay un elemento */
    }
    return NULL;
}

El productor espera en sem_wait(&vacios) si el buffer está lleno. Cuando un consumidor extrae un elemento, hace sem_post(&vacios), despertando al productor.

5. El consumidor

El consumidor espera a que haya un elemento, adquiere el mutex, extrae el dato, libera el mutex y señala que hay un espacio vacío:

consumidor
void *consumidor(void *arg) {
    for (int i = 0; i < N; i++) {
        sem_wait(&llenos);       /* esperar elemento */
        pthread_mutex_lock(&mutex);
        int dato = extraer(buffer);  /* extraer del buffer */
        pthread_mutex_unlock(&mutex);
        sem_post(&vacios);       /* señalar que hay espacio */
        consumir(dato);          /* procesar dato */
    }
    return NULL;
}

El consumidor espera en sem_wait(&llenos) si el buffer está vacío. Cuando un productor inserta un elemento, hace sem_post(&llenos), despertando al consumidor.

6. Por qué el orden de los wait importa

El orden de los sem_wait es crítico. El productor debe hacer sem_wait(&vacios) antes de pthread_mutex_lock, y el consumidor debe hacer sem_wait(&llenos) antes de pthread_mutex_lock.

La razón es: si se bloquea el mutex primero y luego se espera en el semáforo, se puede producir un deadlock.

El orden es crítico

Si el productor bloquea el mutex y luego espera en vacios, y el consumidor espera en llenos y necesita el mutex para hacer post en vacios, se produce un deadlock. Por eso, los sem_wait deben ir antes de los mutex_lock.

7. El deadlock por orden incorrecto

Veamos qué ocurre si el orden es incorrecto:

orden incorrecto (deadlock)
/* Productor INCORRECTO. */
void *productor_malo(void *arg) {
    for (int i = 0; i < N; i++) {
        int dato = producir();
        pthread_mutex_lock(&mutex);      /* bloquea el mutex primero */
        sem_wait(&vacios);              /* espera espacio (puede bloquearse) */
        insertar(buffer, dato);
        pthread_mutex_unlock(&mutex);
        sem_post(&llenos);
    }
    return NULL;
}

/* Consumidor INCORRECTO. */
void *consumidor_malo(void *arg) {
    for (int i = 0; i < N; i++) {
        pthread_mutex_lock(&mutex);      /* bloquea el mutex primero */
        sem_wait(&llenos);              /* espera elemento (puede bloquearse) */
        int dato = extraer(buffer);
        pthread_mutex_unlock(&mutex);
        sem_post(&vacios);
        consumir(dato);
    }
    return NULL;
}

Escenario de deadlock:

  1. El buffer está lleno. vacios = 0, llenos = CAPACIDAD. estado
  2. El productor bloquea el mutex y luego hace sem_wait(&vacios). Se bloquea (vacios = 0). Tiene el mutex. bloqueo
  3. El consumidor intenta bloquear el mutex. Se bloquea (el mutex está ocupado). bloqueo
  4. Deadlock: el productor espera a que vacios > 0, pero el consumidor no puede hacer post(vacios) porque no tiene el mutex. El consumidor espera el mutex, pero el productor no lo libera. deadlock

Este deadlock ocurre porque el productor bloquea el mutex y luego se bloquea en el semáforo, reteniendo el mutex mientras espera. El consumidor no puede liberar el mutex porque no puede adquirirlo.

El orden incorrecto causa deadlock

Si se bloquea el mutex antes de esperar en el semáforo, se puede producir un deadlock. La solución es esperar en el semáforo primero y luego bloquear el mutex.

8. Código completo

productor_consumidor.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <semaphore.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

int buffer[CAPACIDAD];
int frente = 0;
int fondo = 0;

sem_t mutex;
sem_t vacios;
sem_t llenos;

void insertar(int dato) {
    buffer[fondo] = dato;
    fondo = (fondo + 1) % CAPACIDAD;
}

int extraer(void) {
    int dato = buffer[frente];
    frente = (frente + 1) % CAPACIDAD;
    return dato;
}

void *productor(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < ITEMS_POR_PRODUCTOR; i++) {
        int dato = id * 100 + i;  /* generar dato */
        sem_wait(&vacios);          /* esperar espacio (correcto: antes del mutex) */
        pthread_mutex_lock(&mutex);
        insertar(dato);
        printf("Productor %d: produjo %d\n", id, dato);
        pthread_mutex_unlock(&mutex);
        sem_post(&llenos);          /* señalar elemento */
    }
    return NULL;
}

void *consumidor(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < (NUM_PRODUCTORES * ITEMS_POR_PRODUCTOR) / NUM_CONSUMIDORES; i++) {
        sem_wait(&llenos);          /* esperar elemento (correcto: antes del mutex) */
        pthread_mutex_lock(&mutex);
        int dato = extraer();
        printf("Consumidor %d: consumió %d\n", id, dato);
        pthread_mutex_unlock(&mutex);
        sem_post(&vacios);          /* señalar espacio */
    }
    return NULL;
}

int main(void) {
    sem_init(&mutex, 0, 1);
    sem_init(&vacios, 0, CAPACIDAD);
    sem_init(&llenos, 0, 0);

    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);

    sem_destroy(&mutex);
    sem_destroy(&vacios);
    sem_destroy(&llenos);
    return 0;
}

9. Propiedades

Propiedad¿La cumple?Comentario
Exclusión mutuaSíEl mutex protege el acceso al buffer.
Sin deadlockSí (con orden correcto)Los sem_wait van antes de los mutex_lock.
Sin inaniciónSí (con FIFO)Los hilos se despiertan en orden FIFO.
Buffer acotadoSíLa capacidad es fija.
Productores y consumidores concurrentesSíPueden ejecutarse simultáneamente.

10. Simulador interactivo

Este simulador muestra el buffer acotado con productores y consumidores:

Simulador · productor-consumidor

PasoHiloOperaciónBufferEstado

11. Resumen

12. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué es el problema del productor-consumidor?

Coordinar productores que generan datos y consumidores que los procesan, con un buffer acotado.

2. ¿Cuántos semáforos se usan en la solución?

Tres: mutex (exclusión mutua), vacíos (espacios disponibles), llenos (elementos en el buffer).

3. ¿Cuál es el valor inicial de cada semáforo?

mutex = 1, vacíos = CAPACIDAD, llenos = 0.

4. ¿Por qué el orden de los wait importa?

Porque si se bloquea el mutex primero y luego se espera en el semáforo, se puede producir deadlock.

5. ¿Qué es el deadlock por orden incorrecto?

El productor bloquea el mutex y espera espacio; el consumidor espera el mutex. Ambos se bloquean.

6. ¿Cuál es la invariante del buffer acotado?

vacios + llenos = CAPACIDAD.

7. ¿Qué hace el productor?

Espera espacio (vacios), bloquea mutex, inserta, libera mutex, señala elemento (llenos).

8. ¿Qué hace el consumidor?

Espera elemento (llenos), bloquea mutex, extrae, libera mutex, señala espacio (vacios).

9. ¿Por qué se usa mutex además de los semáforos?

Para proteger el acceso al buffer (insertar/extraer) de condiciones de carrera.

10. ¿Qué ocurre si el buffer está lleno?

El productor se bloquea en sem_wait(&vacios) hasta que un consumidor extraiga un elemento.

Para practicar

  1. Implementa el productor-consumidor y ejecuta con 2 productores y 2 consumidores.
  2. Intercambia el orden de los wait (mutex antes de semáforo) y observa el deadlock.
  3. Modifica la capacidad del buffer y observa el efecto.
  4. Añade más productores que consumidores y observa.
  5. Usa sem_trywait para verificar si hay espacio/elementos sin bloquear.
  6. Investiga cómo se implementa el productor-consumidor en Java (BlockingQueue).