Ejemplo con ThreadPoolExecutor

De ChuWiki
Saltar a: navegación, buscar

Desde java 1.0 tenemos la clase Thread para lanzar hilos. Sin embargo desde java 1.5 tenemos la interface Executor con varias clases que la implementan con intención de mejorar la clase Thread. Veamos aquí un ejemplo de la clase ThreadPoolExecutor


Pool de Thread

Un Thread es algo relativamente costoso de crear, por lo que crear un Thread cada vez que lo necesitamos, no es algo eficiente. Es más eficiente si tenemos un conjunto de Thread ya creados y los vamos reutilizando para diversas tareas que queramos que se ejecuten en hilos separados. Para ello, tenemos la clase ThreadPoolExecutor, que contiene un número configurable de Thread a los que podremos ir pasando tareas a ejecutar.

Habitualmente esta clase no se instancia directamente, sino que suele obtenerse a partir de la clase Executors, de la siguiente forma

ExecutorService executor = Executors.newFixedThreadPool(2);

siendo 2 el número de hilos que queremos que tenga preparados la clase dentro. Este método nos devuelve una interface ExecutorService, a la que podemos ir añadiendo tareas a ejecutar y a las que podemos preguntar su estado, si han terminado, pedirles que terminen, etc.

Sólo hay un detalle importante a tener en cuenta. En este ejemplo, hemos creado el executor con dos hilos, por lo que sólo podrá ejecutar dos tareas simultáneamente, una en cada hilo. Si añadimos más tareas, se quedarán encoladas y no se ejecutarán hasta que uno de los hilos quede libre.

Añadir Runnable

Si la tarea a realizar no devuelve ningún resultado, esta tarea bastará que implemente la interface Runnable e implemente su método run(). Por ejemplo

public class Work implements Runnable {
   public void run() {
      // La tarea a realizar.
   }
}

Bastará con añadir una instancia de esta tarea Runnable al executor, así

executor.execute(new Work());

listo, esto empezará a ejecutar la tarea en uno de los hilos disponibles, cuando lo haya, hasta que termine.


Añadir Callable

Si la tarea debe devolver un resultado, una forma de facilitar su lectura después es hacer que la tarea implemente Callable, en vez de Runnable, así

public class OtherWork implements Callable<String> {

   public String call() throws Exception {
      // tarea a realizar
      return "el resultado";
   }

}

En el ejemplo, el resultado que debe devolver la tarea es un String, así que nuestra clase OtherWork implementa Callable<String>. Esto le obliga a implementar un métdoo call() que devuelva un String. En este método se realiza la tarea que sea y se devuelve, con un return, el resultado.

Para lanzar esta tarea, en vez de el método execute() del executor, se utiliza el método submit(), que devuelve un Future, así

Future<String> future = executor.submit(new OtherWork());

Es a este future al que podemos preguntar si la tarea esta terminada (isDone()) y recoger luego el resultado con el método get(). El método get() se queda bloqueado hasta que la tarea termine y el resultado esté disponible, por lo que si no queremos quedarnos bloqueados, debemos usar previamente el método isDone(), o bien poner un "timeout" al método get()

Es decir, bien preguntando con isDone() ...

while (!future.isDone()) {
   // Esperar un poco o hacer otras cosas.
}
String resultado = future.get();

o bien poniendo un timeout ....

try {
   String resultado = future.get(1,TimeUnit.SECONDS);
} catch (TimeoutException e) {
   // Se ha pasado el tiempo sin obtener resultado
}


Utilidades de ExecutorService

Como executor es en realidad un ExecutorService, tenemos ciertos métodos para controlar los hilos que están dentro. Mencionamos sólo por encima algunos de ellos

  • shutdown(). Este método indica al executor que no acepte más tareas. No termina las que están en ejecución, sino que se las deja terminar de forma natural, incluyendo a las que están en cola de espera por un hilo disponible. Cuando todas terminen de forma natural, se matan los hilos y se finaliza totalmente el executor. Es importante llamar a este método cuando terminemos de usar el executor, puesto que si no, los hilos quedarán vivos.
  • shutdownNow(). Este método interrumpe todos los hilos que están en ejecución. Ya es cuestión de cada una de nuestras tareas el cómo traten esa interrupción para terminar de forma adecuada. También elimina de la cola todas las tareas que no hayan empezado a ejecutarse todavía. Al depender de cómo se comporten nuestras tareas ante una interrupción, no hay ninguna garantía de que los hilos realmente mueran o terminen.
  • awaitTermination(). Este método, después de una llamada a shutdown(), se queda bloqueado a la espera de que todos los hilos terminen. Se le pasa como parámetro el tiempo máximo de espera. Devuelve true si todos los hilos han terminado, false si ha saltado el tiempo de espera sin que hayan terminado los hilos.


Enlaces