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:
1class 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
15class 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
36class 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
51class 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:
- El cocinero puede preparar múltiples hamburguesas antes de que el mesero sirva una.
- El mesero puede servir la misma hamburguesa más de una vez.
- 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:
1class 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á lista17 }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 hamburguesa28 System.out.println("Mesero sirvió hamburguesa #" + hamburguesa);29 notify(); // Notificar al chef que la hamburguesa fue servida30 return hamburguesa;31 }32}
En esta nueva implementación vemos:
- Usamos una variable booleana
hamburguesaLista
para controlar el estado. - El chef espera si ya hay una hamburguesa lista, evitando la sobreproducción.
- El mesero espera si no hay hamburguesa lista, evitando servir algo que no existe.
- 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.