Bueno, a actualizar que van siendo horas. En primer lugar, pedir disculpas por la tardanza a mis lectores habituales (¡JA!) pero cuando el tiempo aprieta hay que priorizar :)

Hoy vamos a ver un ejemplo de modelo Productor-Consumidor, que como sabéis representa un problema típico de sincronización en el que tenemos por un lado un hilo que produce datos a procesar y otro hilo que los recoge. Los problemas aparecen por ejemplo, al no poder garantizar que el productor producirá a suficiente velocidad para el consumidor, o viceversa, al no poder garantizar que el consumidor se encontrará siempre algún dato que procesar, con lo que podría estar procesando repetidas veces el mismo dato, o detenerse, etc.

En Java tenemos una clase muy útil que nos permite implementar muy fácilmente este modelo. Se trata de la clase LinkedBlockingQueue, consistente en una cola bloqueante, la cual se comporta de forma que si intentamos recuperar un dato cuando está vacía, se queda esperando, mientras que si tiene algún dato lo devuelve inmediatamente.

Con esto se consigue que el hilo productor pueda producir a su ritmo, almacenando en la cola, mientras el hilo consumidor recoge los datos cuando los haya, quedándose a la espera cuando no.

Veamoslo con un ejemplo. Considero que el código está lo suficientemente comentado, pero de todas formas lo explicaré con más detalle:


package productor.consumidor;



import java.util.Random;

import java.util.concurrent.LinkedBlockingQueue;



/**

* Ejecuta tareas en segundo plano a medida que le van llegando a una cola de

* tareas.

*

* @author McQueen

*/

public class BackgroundThread extends Thread {

/**

* Cola bloqueante de tareas. Cuando se intenta obtener un elemento de dicha

* cola y ésta está vacía, se queda a la espera de que se añada algún

* elemento.

*/

private static LinkedBlockingQueue<Task> taskQueue = null;



/**

* Detendremos la tarea un tiempo aleatorio para simular una tarea costosa

* para el sistema.

*/

private static Random random = new Random(System.currentTimeMillis());



/**

* Constructor, inicializa la cola bloqueante.

*/

public BackgroundThread() {

taskQueue = new LinkedBlockingQueue<Task>();

}



/**

* Prepara una tarea almacenandola en la cola, para ejecutarla tan pronto

* "le toque".

*/

public void executeTask() {

Task task = new Task();

taskQueue.add(task);

}



/**

* Ejecuta la tarea del hilo.

*/

public void run() {

int time = 0;

while (true) {

try {

/* Esperamos a que se pueda ejecutar la tarea. */

Task task = taskQueue.take();

/* Se ejecuta. */

task.execute();

time = random.nextInt(100) + 1;

/* Se duerme el hilo varios milisegundos (entre 1 y 100) */

sleep(time);

System.out.println("Ejecutada tarea en " + time

+ " milisegundos");

} catch (InterruptedException e) {

System.out.println("Hilo interrumpido");

}

}

}

}



/**

* Clase que realiza una tarea concreta. Se retarda un tiempo para que el

* ejemplo tenga sentido.

*/

class Task {

void execute() {

System.out.println("Ejecutando nueva tarea");

}

}



Esta clase tiene un método executeTask que, pese a su nombre, no ejecuta realmente la tarea, sino que la encola a la espera de que pueda ejecutarse (las tareas se ejecutan de una en una, ya que las estamos lanzando todas en un mismo hilo). Para crear las tareas he utilizado una clase de ejemplo Task, pero sobra decir que hay mil formas de hacerlo.

A continuación el método run que ejecuta el código principal del hilo, simplemente intenta recoger un elemento (una tarea, una Task) de la cola. Si lo consigue, continúa. Si no lo consigue, se queda a la espera. Una vez lo consigue se ejecuta la tarea (en este caso solo se muestra una línea por consola).

A continuación viene el código que normalmente sobraría en una aplicación real, y que utilizamos para simular un proceso costoso en tiempo, durmiendo el hilo consumidor entre 1 y 100 milisegundos de forma aleatoria (por darle algo más de dinamismo xD).

La InterruptedException que se puede producir notificaría que se ha interrumpido el hilo por ejemplo por algún otro hilo distinto (ya estuviese esperando, ejecutando...). Si se produce puede avisarse a algún método del evento, mostrar algún error, iniciar otro hilo que ejecute alguna otra tarea, detener la aplicación, o, simplemente, no hacer nada.

A continuación veremos el código correspondiente a la parte productora. En este caso lo hemos puesto directamente en el Main, pero evidentemente podría estar en otra clase específica.


package productor.consumidor;



public class Main {

public static void main(String[] args) {

int n = 10;

BackgroundThread back = new BackgroundThread();



/*

* Con 'true' se evita que el programa siga en ejecución si el hilo

* principal termina, por eso no llegan a ejecutarse todas las tareas.

*

* Con 'false' el hilo en background se sigue ejecutando aunque el hilo

* principal termine, por tanto se ejecutan todas las tareas.

*/

back.setDaemon(false);



/* Comenzamos el hilo. */

back.start();



while (n > 0) {

System.out.println("Añadiendo tarea, quedan " + n + " tareas");

back.executeTask();

n--;

}



/*

* OJO, si hemos puesto el setDaemon a false el programa sigue

* ejecutandose aún al llegar al final del Main (el hilo en background

* seguirá esperando tareas indefinidamente).

*/

}

}



Simplemente creamos el hilo y lo iniciamos. Lanzamos 10 tareas dentro de un bucle, de forma que se van almacenando en la cola y ejecutando tan pronto como les sea posible.

La salida por consola en un caso así dependerá de la máquina y de si toqueteamos y jugamos con los milisegundos y con el número de tareas a ejecutar.

Atención a la línea back.setDaemon(false);. Como bien cuentan desde Sun, "The Java Virtual Machine exits when the only threads running are all daemon threads" o lo que es lo mismo, la máquina virtual termina cuando los únicos hilos en ejecución son hilos "daemon".

Por lo tanto, si el hilo lo ejecutamos como "daemon" (true), en cuanto el hilo principal llegue al final la aplicación terminará, y no llegarán a ejecutarse todas las tareas de la cola. A continuación la salida mostrada en mi máquina (en una de tantas ejecuciones, ya que el resultado varía).

Añadiendo tarea, quedan 10 tareas
Ejecutando nueva tarea
Añadiendo tarea, quedan 9 tareas
Añadiendo tarea, quedan 8 tareas
Añadiendo tarea, quedan 7 tareas
Añadiendo tarea, quedan 6 tareas
Añadiendo tarea, quedan 5 tareas
Añadiendo tarea, quedan 4 tareas
Añadiendo tarea, quedan 3 tareas
Añadiendo tarea, quedan 2 tareas
Añadiendo tarea, quedan 1 tareas
Ejecutada tarea en 19 milisegundos
Ejecutando nueva tarea


Comienza dos tareas pero solo llega a finalizar una. Las otras 9 quedan sin ejecutarse y el programa se termina.

Si ejecutamos el programa con setDaemon (false) como en el ejemplo, el hilo consumidor sigue corriendo aunque el Main haya finalizado, por lo que se llegan a ejecutar todas las tareas.

Añadiendo tarea, quedan 10 tareas
Ejecutando nueva tarea
Añadiendo tarea, quedan 9 tareas
Añadiendo tarea, quedan 8 tareas
Añadiendo tarea, quedan 7 tareas
Añadiendo tarea, quedan 6 tareas
Añadiendo tarea, quedan 5 tareas
Añadiendo tarea, quedan 4 tareas
Añadiendo tarea, quedan 3 tareas
Añadiendo tarea, quedan 2 tareas
Añadiendo tarea, quedan 1 tareas
Ejecutada tarea en 19 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 66 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 58 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 3 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 90 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 3 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 61 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 48 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 90 milisegundos
Ejecutando nueva tarea
Ejecutada tarea en 57 milisegundos


En este caso, incluso cuando se han ejecutado las 10 tareas el hilo sigue ejecutándose a la espera de nuevas tareas que en este caso nunca llegarán.

Como siempre espero que a alguien le sirva.
Saludetes y hasta el mes que viene (que nooooo, intentaré actualizar antes xD).

1 Comment:

  1. Anónimo said...
    Gracias !! Muy util y claro !
    Me ayudo muchisimo !

Post a Comment