sábado, 17 de noviembre de 2012

Thread Pools en Java

votar

En otras entradas he explicado que son los hilos o threads, cómo crearlos y cómo funcionan. En esta entrada voy a tratar el tema de las thread pools, literalmente “piscinas de hilos”. ¿Por qué se llaman así? Bueno...ni idea. Pero ya que le han dado ese nombre, voy a utilizarlo en mi metáfora para explicarte en que consisten.

Imagínate una thread pool como un grupo de nadadores vagueando al borde de una piscina. De vez en cuando llega un entrenador con la orden de que uno de los nadadores tiene que recorrer la piscina de arriba abajo. A veces todos los nadadores reciben esas órdenes, un entrenador por cada nadador. En ocasiones hay más entrenadores que nadadores, y los últimos en llegar tienen que esperar a que vayan regresando los nadadores para ir dándoles de nuevo la orden de recorrer la piscina. Y otras veces sólo algunos nadadores están nadando mientras los otros están jugando a las cartas, esperando a que llegue algún entrenador.

Ahora sustituye “entrenador” por “tarea” y “nadador” por “hilo” y ya tienes una thread pool.

“Vale”, te preguntarás, “¿y por qué hacer una thread pool? ¿No es mejor ir creando los hilos según los necesitemos?”. Pues eso va a depender del programa en cuestión. Ten en cuenta que cada hilo que creas ocupa lugar en la memoria. Si tienes un programa que sólo va a tener tres o cuatro tareas distintas, pues bueno, creas tres o cuatro hilos. Pero, ¿y si es un programa con cientos o miles de tareas?. Cientos de hilos ocuparán bastante memoria. Y lo que es peor: tú tendrás que escribir el código para crear todos esos hilos.Créeme, es buena práctica separar en ese caso las tareas de los hilos en tu código. Y tú trabajarás menos.

Además, está la cuestión de la eficiencia. Ya sé lo que estás pensando: “¿No es más eficiente tener un hilo para cada tarea, en lugar de tener tareas esperando que haya un hilo libre?” Volvamos a nuestro ejemplo de la piscina.

Supón que en la piscina hay cien nadadores y que todos reciben la orden de nadar a la vez. ¿Has tratado alguna vez de nadar en una piscina abarrotada? ¿Crees que alguno de los nadadores conseguiría un récord de velocidad? Pues tu programa lo mismo. El sistema en el que se ejecute tendrá que repartir sus recursos entre todos los hilos a la vez.

Hagamos ahora que los nadadores sean sólo diez, con las mismas cien tareas. Es cierto que hay algunas tareas que tendrán que esperar a que los nadadores vayan regresando para realizarse, pero como los nadadores pueden ir y volver mucho más rápido, las primeras tareas serán “visto y no visto” y el tiempo de espera de las últimas será similar al producido si todas tuviesen un hilo propio. Y eso si todas las órdenes se reciben a la vez, porque si los entrenadores van llegando de forma separada, el tiempo de espera de los últimos no será mayor que el de los primeros.

Bueno, ya sabemos lo que es una thread pool, ¿pero cómo se utiliza?. Ah, aquí entran en juego los “Ejecutores”. No, no es una película de Schawzenegger. Los ejecutores en java son simplemente los que se encargan de manejar la ejecución de las tareas.

Java tiene una clase, Executors, con métodos estáticos para la creación de thread pools, y tres interfaces:
  • Executor, que simplemente ejecuta las tareas
  • ExecutorService, subinterfaz de la anterior que añade nuevos métodos para controlar las tareas.
  • ScheduledExecutorService, subinterfaz de ExecutorService que soporta la ejecución de tareas programadas.
Todas ellas forman parte del paquete java.util.concurrent.

A continuación, te voy a mostrar un ejemplo sencillísimo de thread pool. Vamos a crear dos clases, una que implementa la interfaz Runnable, porque los ejecutores trabajan con objetos "runnable" o "callable", y otra que contiene el método principal y donde crearemos una thread pool de 10 hilos.

Tarea.java

public class Tarea implements Runnable{
   private int sleepTime;
   private String name;
   public Tarea(String name){
    this.name = name;//le asignamos un nombre a cada tarea.
    sleepTime = 1000;
 }
   
   public void run(){
 try{
    System.out.printf("El hilo de la tarea "+this.name+" va a dormir durante %d milisegundos.\n",sleepTime);
    Thread.sleep(sleepTime);//hacemos que cada hilo duerma durante 1 segundo
 }catch(InterruptedException exception){
    exception.printStackTrace();
 }
 System.out.println("Este hilo ya ha dormido bastante");
 }
}


EjemploThreadPool.java

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EjemploThreadPool{
   public static void main (String args[]){
    System.out.println("Comienza la ejecución");
    ExecutorService ex = Executors.newFixedThreadPool(10);
 Tarea t;
 for(int i = 0;i<200; i++){
    t = new Tarea(""+i);
 
    ex.execute(t);
 }
    ex.shutdown();
    
   }
}


¿Qué es lo que hace este programa? No gran cosa. Se limita a escribir en la consola que el hilo que está ejecutando cada tarea, a las que hemos asignado un número, va a dormir durante 1 segundo. Pero, ¿y lo que has aprendido?

Fíjate que al método execute es al que hay que darle un objeto runnable, y que el método shutdown se asegura de cerrar al finalizar las tareas. Ambos métodos pertenecen a la interfaz ExecutorService. Para crear  la trhead pool hemos empleado el método newFixedThreadPool(), al que se le da un número específico de hilos. Pero existen más, como el newCachedThreadPool(), ambos de la clase Executors.

Para ampliar conocimientos, te recomiendo el tutorial sobre concurrencia de Oracle, y el libro "Java Threads", de S. Oaks y H. Wong, publicado por O´Reilly.

Y como siempre, si tienes alguna duda o comentario, ya sabes donde estamos.

14 comentarios:

  1. oye y como se maneja mas de un hilo con diferente tarea y que se ejecute en diferente periodo de tiempo

    ResponderEliminar
  2. Hola, Carlos.
    Cada tarea distinta tiene que ser un objeto distinto de tipo Runnable o Callable, con su propio método run sobreescrito. Esto se puede hacer creando una clase para cada tarea, bien con clases anónimas dentro del constructor de la ThreadPool.
    En cuanto a programar las tareas, depende de si necesitas sincronizarlas o no, es decir, si tienes que esperar a que termine una para que se incie la otra. En ese caso, te recomiendo que eches un vistazo a esta entrada que escribí sobre sincronización de hilos: Threads II - Synchronized, que encontrarás a la derecha en el Archivo del Blog.
    Por lo demás, la mejor opción para programar tareas es utilizar la clase ScheduledThreadPoolExecutor, que cuenta con varios métodos que te permiten hacerlo.
    De todas formas, como la concurrencia y el manejo de hilos son temas bastante complejos, especialmente para los principiantes a los que está dirigido este blog, intentaré dedicarle una o dos nueva entradas al asunto, para hablar más a fondo sobre tareas en cola, tareas programadas, etc.
    Un saludo.

    ResponderEliminar
  3. Gracias Sonia por la explicación, me ha sido de gran ayuda...

    ResponderEliminar
  4. Justo lo que estaba buscando, las demás paginas lo dan en ingles y ademas no se les entiende muy bien, muchas gracias.

    ResponderEliminar
  5. Hola excelente y muy bien explicado para entender como trabajar con threads.

    ResponderEliminar
    Respuestas
    1. ¡Gracias, Andrés! Me alegro de que te haya gustado :-)

      Eliminar
    2. Este comentario ha sido eliminado por el autor.

      Eliminar
  6. Hola, gracias por la explicación fue muy sencilla

    ResponderEliminar