Saltearse al contenido

Productor-Consumidor

El problema del productor-consumidor

El problema del productor-consumidor es un problema de sincronización entre diferentes procesos. Hay tres entidades en este problema:

  • Un productor
  • Un consumidor
  • Un buffer (almacén)

Tanto el productor como el consumidor acceden al buffer para producir y consumir elementos, respectivamente. El productor produce elementos y los almacena en el buffer, mientras que el consumidor consume elementos del buffer.

Si el buffer está vacío, entonces el consumidor espera a que el productor coloque un elemento, que consumirá después de que el productor lo haya colocado.

El buffer de memoria tiene un tamaño fijo. Si está lleno, el productor espera a que el consumidor consuma un elemento antes de colocar uno nuevo. El productor y el consumidor no pueden acceder al buffer al mismo tiempo, es decir, es mutuamente exclusivo. Cada proceso debe esperar a que el otro termine su trabajo en el búfer antes de poder acceder a él.

Implementación en Java

Para ilustrar cómo funciona la comunicación entre hilos en la práctica, vamos a explorar un ejemplo práctico. Imaginemos una cocina de un restaurante de comida rápida con dos empleados: un cocinero que prepara hamburguesas (el productor) y un mesero que las sirve (el consumidor).

Podemos ver un primer enfoque al problema en el siguiente código:

1
class Cocina {
2
private int hamburguesa;
3
4
public synchronized void prepararHamburguesa(int numero) {
5
this.hamburguesa = numero;
6
System.out.println("Chef preparó hamburguesa #" + numero);
7
}
8
9
public synchronized int servirHamburguesa() {
10
System.out.println("Mesero sirvió hamburguesa #" + hamburguesa);
11
return hamburguesa;
12
}
13
}
14
15
class Chef implements Runnable {
16
Cocina cocina;
17
18
public Chef(Cocina cocina) {
19
this.cocina = cocina;
20
new Thread(this, "Chef").start();
21
}
22
23
public void run() {
24
int i = 0;
25
while(true) {
26
try {
27
Thread.sleep(1000);
28
} catch (InterruptedException e) {
29
e.printStackTrace();
30
}
31
cocina.prepararHamburguesa(i++);
32
}
33
}
34
}
35
36
class Mesero implements Runnable {
37
Cocina cocina;
38
39
public Mesero(Cocina cocina) {
40
this.cocina = cocina;
41
new Thread(this, "Mesero").start();
42
}
43
44
public void run() {
45
while(true) {
46
cocina.servirHamburguesa();
47
}
48
}
49
}
50
51
class Restaurante {
52
public static void main(String args[]) {
53
Cocina cocina = new Cocina();
54
Chef chef = new Chef(cocina);
55
Mesero mesero = new Mesero(cocina);
56
System.out.println("Restaurante en funcionamiento");
57
}
58
}

Veamos lo que ocurre en esta implementación, tiene varios problemas:

  1. El cocinero puede preparar múltiples hamburguesas antes de que el mesero sirva una.
  2. El mesero puede servir la misma hamburguesa más de una vez.
  3. El cocinero y el mesero no están sincronizados.

Para resolver estos problemas, necesitamos implementar un mecanismo de comunicación entre los hilos. En Java, podemos hacerlo utilizando los métodos wait(), notify() y notifyAll().

Implementación usando wait() y notify()

Para solucionar los problemas anteriores, necesitamos hacer que el cocinero espere a que el mesero sirva la hamburguesa antes de preparar otra. Del mismo modo, el mesero debe esperar a que el cocinero prepare una hamburguesa antes de servirla.

Para lograr esto, vamos a modificar la clase Cocina de la siguiente manera:

1
class Cocina {
2
private int hamburguesa;
3
private boolean hamburguesaLista = false;
4
5
public synchronized void prepararHamburguesa(int numero) {
6
while(hamburguesaLista) {
7
try {
8
wait();
9
} catch (InterruptedException e) {
10
e.printStackTrace();
11
}
12
}
13
this.hamburguesa = numero;
14
hamburguesaLista = true;
15
System.out.println("Chef preparó hamburguesa #" + numero);
16
notify(); // Notificar al mesero que la hamburguesa está lista
17
}
18
19
public synchronized int servirHamburguesa() {
20
while(!hamburguesaLista) {
21
try {
22
wait();
23
} catch (InterruptedException e) {
24
e.printStackTrace();
25
}
26
}
27
hamburguesaLista = false; // Se consumió la hamburguesa
28
System.out.println("Mesero sirvió hamburguesa #" + hamburguesa);
29
notify(); // Notificar al chef que la hamburguesa fue servida
30
return hamburguesa;
31
}
32
}

En esta nueva implementación vemos:

  1. Usamos una variable booleana hamburguesaLista para controlar el estado.
  2. El chef espera si ya hay una hamburguesa lista, evitando la sobreproducción.
  3. El mesero espera si no hay hamburguesa lista, evitando servir algo que no existe.
  4. Ambos métodos usan notify() para avisar al otro hilo cuando pueden proceder.

Esta implementación asegura una perfecta sincronización entre producción y consumo, evitando los problemas de la versión anterior.