sábado, 14 de abril de 2012

Threads I - Formas de crear threads

votar
   Los threads, llamados en castellano hilos o hebras, son partes del código que se encargan de ejecutar tareas de forma simultánea en varios procesadores o de forma conmutada si se trata de un solo procesador ( aunque en este último caso, siendo las velocidades las que son, parezca que dichas tareas se ejecutan todas a la vez).

   Como este blog esta pensado para novatos, vamos a ir poco a poco y explicar de forma sencilla  y con ejemplos cómo se pueden crear threads en un programa.

   Hay que tener en cuenta que la tarea que ejecutan estas hebras pueden ser la misma, o bien pueden ser tareas distintas. Por ejemplo, supongamos que queremos hallar simultáneamente el factorial de tres números. En este caso lo haremos extendiendo la clase Thread:


public class Factorial extends Thread{
   long nf;
   Factorial(long n){
      nf = n;
   }
   

   public static void main (String[] args){
      Factorial a = new Factorial(10);
      Factorial b = new Factorial(15);
      Factorial c = new Factorial(20);
      a.setName("a");
      b.setName("b");
      c.setName("c");
      a.start();
      b.start();
      c.start();
   }
      public void run(){
         
         long fact = 1;
         while(nf>1){
            fact *= nf--;
         }
         System.out.println("El factorial de " + Thread.currentThread().getName() + " es: "+ fact);
      }
   
}

   Vemos en este ejemplo que hemos extendido la clase Thread y a continuación hemos creado tres instancias de la subclase creada Factorial, cada una de las cuales ejecuta la misma tarea que las demás, pero sobre distintos números. La clase Thread tiene varios métodos interesantes, como setName() y getName(), para nombrar los distintos hilos (aunque por defecto son numerados a partir del 0) y currentThread() para saber que hilo se está ejecutando en un momento preciso. 


   Pero además, la clase Thread implementa la interfaz Runnable, que sólo contiene un método: run().
   El método run() es la tarea que queremos realizar; por ello debemos sobreescribirlo con lo que queremos que hagan nuestros threads. Pero fijaos bien, para que una hebra comience a hacer la tarea que queremos, nunca, jamás, podemos utilizar run().


      a.run(); //¡JAMÁS!


      Lo que usamos es el método start(), como vemos en el ejemplo:


      a.start(); //correcto.


      Este método es el que se encarga de llamar al método run().


      La forma que hemos visto en este ejemplo de crear threads es la más sencilla, pero no es la más habitual. ¿Por qué?. Pues para empezar, porque si extiendes la clase Thread, ya no puedes extender ninguna otra clase, y eso limita bastante. Además, lo correcto es extender una clase sólo cuando se quiere un comportamiento más especializado de la misma.


     La forma de crear hebras que nos va a permitir mayor flexibilidad para trabajar es crear una clase que implemente la interfaz Runnable, como hace la clase Thread, y luego pasar las referencias de sus instancias como argumentos a las instancias de Thread.


      Lo entenderéis más fácilmente con el siguiente ejemplo:






      
public class DemoThread{
   public static void main(String[] args)throws InterruptedException{
      Runnable r1 = new TextoUno(); //creamos una instancia de una clase que implementa Runnable
      Runnable r2 = new TextoDos(); //creamos una instancia de otra clase que también implementa Runnable
   
      /*creamos dos instancias de la clase Thread y le asignamos a cada una "tarea" distinta*/
      Thread t1 = new Thread(r1); 
      Thread t2 = new Thread(r2);

      t1.start(); //t1 comienza su ejecución
      t1.join(); //t1 obliga a t2 a esperar hasta que acaba su ejecución
      t2.start(); //t2 comienza su ejecución
   }
}

class TextoUno implements Runnable{
   public void run(){ //sobreescribimos el método run()
      System.out.println("Esto es una demostración");
      for(int i=0; i<10; i++){
         System.out.println("Estamos esperando...");
         try{
            Thread.sleep(1000); //ralentizamos la ejecución
         }catch (InterruptedException e){
            System.out.println("¡Madre mía!");
         }
      }
   }
}

class TextoDos implements Runnable{
   public void run(){ //sobreescribimos el método run() de forma distinta
      System.out.println(Thread.currentThread().getName());
      System.out.println("Por fin acabó t1");
   }
}

    En este segundo ejemplo tenemos dos hilos, cada uno de los cuales hace una cosa distinta. Pero, atención: el orden en el que nosotros iniciamos los hilos no es necesariamente el orden que ellos van a seguir. 


      Imaginemos que tenemos dos hilo: t1, que tiene como salida los números del 1 al 10, y t2, que tiene como salida las vocales, e iniciamos primero t1 y luego t2. Puede que nos salgan primero los números y después las vocales, o puede que nos salga cualquier mezcla entre números y vocales. Es más,  si volvemos a ejecutar el código, el resultado no tiene porqué ser el mismo. Entonces, ¿cómo hacemos si queremos que los hilos se ejecuten en un orden específico?. Hay varias formas de conseguirlo. La más eficaz es con el método join() que hemos utilizado aquí. Cuando este método es llamado por un hilo le indica a los demás que tienen que esperar a que dicho hilo acabe. En un pr0grama tan sencillo como este, lo hemos aplicado a toda la ejecución, pero en otros más complejos se puede dejar que los hilos comienzan simultáneamente y se entrecrucen, y usarlo en el momento necesario de la ejecución.


      Hay otras formas, no tan eficaces, de intentar "manejar los hilos" (mal juego de palabras, ya sé). Puede usarse por ejemplo el método sleep() que pausa la ejecución de un hilo durante unos milisegundos o nanosegundos. Tanto el método sleep() como el método join() lanzan una InterruptedException, que hay que declarar o manejar. Si no entiendes esto, hecha un vistazo a la entrada sobre excepciones en java.


   También se pueden establecer prioridades, que pueden o no funcionar como el programador espera. La manera de hacerlo es con el método setPriority( int priority) con valores del 1 al 10. O se pueden usar las constantes Thread.MAX_PRIORITY, Thread.MIN_PRIORITY y Thread.NORM_PRIORITY. Pero esto tampoco es muy fiable y depende mucho del sistema operativo que se use.


  La moraleja de todo esto, es que nunca des por sentado cómo se van a comportar los threads, o que porque tu programa funcione de una manera en un procesador, funcionará igual en todos. Así que asegúrate de que tu programa multihebra no depende de estos factores. 


   Y si esta falta de control te parece irritante, espera a ver nuestra próxima entrada, que trata de la sincronización.


   Si deseas más información, puedes consultar el libro de Scott Oaks y Henry Wong, Java Threads, publicado por O´Reilly, o los Tutoriales de Oracle.


   

No hay comentarios:

Publicar un comentario en la entrada