Hilos


 

Programación con hilos

Un Thread es un "hilo" de ejecución de un programa. Casi podríamos decir que un Thread o Hilo es un programa en ejecución ya que en general un programa tiene un solo hilo de ejecución. Cuando se ejecuta un programa o un applet el sistema operativo (o el navegador) crea un hilo para ejecutar el programa. Todos los sistemas operativos que se utilizan actualmente (como Windows95 o superior, Linux, etc...) son multihilo, es decir, pueden ejecutar simultáneamente varios hilos (o varios programas si se quiere). Cuando hay varios programas ejecutándose simultáneamente hay varios hilos de ejecución.

En Java es posible y muy fácil crear programas con varios hilos de ejecución, es decir, se pueden crear programas que realizan varias tareas simultáneamente independientemente una de la otra. Por ejemplo, el programa Hilos cuyo código se muestra abajo arranca dos hilos aparte del propio y cada uno de los tres realiza una tarea diferente: uno escribe "PIM" cada segundo durante 7 segundos, otro escribe "PAM" cada medio segundo durante 10 segundos y el último escribe "PUM" cada 0.8 segundos indefinidamente.

Si su navegador reconociese la etiqueta de applet, aquí vería un applet

Este es el resultado en la consola después de ejecutar java unidad06.Hilos:

Este programa merece varios comentarios.

Observe que Hilo1 e Hilo2  realizan todo su trabajo en el método public void run(). Esto es así siempre. Cuando un hilo se echa a andar llamando a su método start(), lo que ocurre es que el sistema le asigna un porcentaje de los recursos del procesador y ejecuta el método run.

Para medir el tiempo trancurrido se utiliza el método estático currentTimeMillis() de la clase System que devuelve el número de milisegundos transcurridos desde las cero horas del día 1º de enero de 1970. Observe que para hacer que un hilo se detenga durante unos milisegundos se llama al método estático sleep(long milisegundos) de la clase Thread. Este método puede producir una interrupción de tipo InterruptedException por lo cual es necesario escribir la llamada dentro de un try ... catch.

El programa termina su ejecución a los 10 segundos. ¿Por qué? Observe que el Hilo2 es un Daemon. La máquina virtual de Java termina su ejecución cuando se llama al método System.exit(int) o cuando todos los hilos que quedan activos son Daemon. Esto último es lo que ocurre en este caso.

Nota: Los recursos asignados a un hilo pueden controlarse utlizando el método setPriority(). En esta lección no entraremos en los detalles de control de las prioridades ni en el tema de los grupos de hilos (ThreadGroups), nos concentraremos únicamente en la utilización de los hilos para crear programas multihilo y en particular animaciones.

Existe un mecanismo muy cómodo y que se utiliza frecuentemente para implementar un hilo en una clase cualquiera. Consiste en implementar la interfaz Runnable ,que consta únicamente del método  public void run(), y crear un hilo (un Thread) con el constructor Thread(Runnable target) pasándole como parámetro this, es decir, se crea el hilo con la llamada new Thread(this). Esto tiene el efecto de que el método public void run() del nuevo hilo es precisamente el de la clase que estamos construyendo. Con este truco podemos hacer que una clase cree y ejecute un hilo sin necesidad de crear aparte una subclase de Thread. El siguiente applet es un cronómetro sencillo que se ha implementado usando este truco.

El alumno debe estudiar detenidamente el código de este applet pues es representativo de los programas en los que se crea un hilo para realizar una tarea adicional a la del hilo principal. En este caso la tarea del hilo principal consiste en leer e interpretar los clics al botón, mientras que la tarea que realiza el hilo adicional es la de representar la aguja del cronómetro en movimiento y escribir los segundos y las décimas de segundo.

Esta es otra versión del cronómetro que (además de ser rojo en vez de naranja) está implementada sin usar la interfaz Runnable.

El alumno puede estudiar el código y comprobar que es más sencilla la primera versión porque consta de una sola clase mientras que la segunda necesita dos.

 

Animación simple

Una de las aplicaciones más utilizadas de la programación con hilos es la animación. En esta unidad estudiaremos la animación simple y dedicaremos toda la unidad 7 a la animación avanzada.

Por animación simple entendemos la que se realiza proyectando imágenes en un mismo lugar. En esta sección estudiaremos las clases simpanim y animador que son applets que sirven para realizar animaciones simples con series de imágenes arbitrarias.

Este es el código de simpanim y un ejemplo de aplicación que muestra varias imágenes de satélite en las que se ven   las nubes sobre Asia en unas horas y fechas determinadas. El resultado de la animación es que se puede apreciar el movimiento y evolución de las nubes..

Si su navegador reconociese la etiqueta de applet, aquí vería un applet

Los botones de parar y continuar pertenecen a la página y controlan el applet (vía JavaScript) usando su nombre y sus métodos públicos stop() y start(). Esta es la forma en que se ha llamado a simpanim para producir la animación.

<applet name="mapas1"
        code="unidad06.simpanim.class" codebase="./"
        width="156" height="120" align="top">
<param name="PASO" value="100">
<param name="PAUSA" value="1000">
<param name="IMA_00" value="images/ws051900.jpg">
<param name="IMA_01" value="images/ws051903.jpg">
<param name="IMA_02" value="images/ws051906.jpg">
<param name="IMA_03" value="images/ws051909.jpg">
<param name="IMA_04" value="images/ws051912.jpg">
<param name="IMA_05" value="ejem13/
imagenes/ws051915.jpg">
</applet>

Y éste es el código html de los botones de parar y continuar.

<FORM>
  <INPUT TYPE=button VALUE="parar" onclick="mapas1.parar()">
  <INPUT TYPE=button VALUE="continuar" onclick="mapas1.continuar()">
</FORM>

Lo primero que debemos observar es que este applet en su definición dice implements Runnable. Esto significa que el applet está obligado a implementar el único método de la interfaz Runnable que es

public void run();

Éste es el método en el que se va a realizar la animación. Una animación es un proceso que transcurre en el tiempo y por lo tanto debe implementarse de manera que deje tiempo libre al procesador entre el despliegue de una imagen y la siguiente, para que pueda atender a otros programas. Esto se logra usando un hilo, o sea un objeto de la clase Thread, al que hemos llamado ani.

Para crear el hilo ani, primero convertimos el applet en un objeto Runnable, con lo que garantizamos que tiene un método public void run(),  y luego dentro del método start() usamos la expresión:

ani=new Thread(this);

con lo que indicamos que el hilo ani usará el método run de nuestro applet como su método run. Otra manera de crear un hilo es crear una extensión o subclase de Thread y sobreescribir su método run, pero esto a veces resulta incómodo pues hay que escribir otra clase, como ya se vio arriba en el caso del cronómetro. El constructor Thread(Runnable r) lo que hace es crear un hilo que tiene al método r.run() como su propio método run. En otras palabras, ani.run() coincide con simpanim.run(). Antes de revisar el contenido del método run analizaremos cómo se leen las imágenes que se van a usar en la animación.

Los nombres de las imágenes que se usan en la animación se pasan al applet mediante los parámetros IMA_00, IMA_01, etc... Dentro del método init() de simpanim puede verse cómo se leen todas estas imágenes. El alumno debe observar que la lectura de imágenes está programada de manera que podrían leerse hasta 100 imágenes (desde IMA_00 hasta IMA_99) y que no es necesario que estén numeradas consecutivamente, el método init lee sólo las que aparezcan en la lista de parámetros del applet. El orden de la animación será el de la numeración y no depende del orden en que aparezcan los parámetros en la llamada al applet. También observamos que para representar la lista de imágenes que se van a proyectar se utiliza un Vector y no una matriz de imágenes. Esto se hace así porque facilita la adopción de las imágenes sin tener que saber de antemano cuántas hay, y también da mayor flexibilidad al usuario del applet.

La animación se realiza en el método public void run(). Allí se crea un ciclo infinito que va proyectando las imágenes una tras otra. Hay una espera de paso milisegundos entre una imagen y la otra. Para lograr esta espera se utiliza el método Thread.sleep(paso) que detiene la ejecución del hilo ani durante paso milisegundos. Cuando se llega al final de la lista de imágenes, se realiza una espera con Thread.sleep(pausa) y se vuelve a comenzar la animación. Es muy impotante usar el método sleep del para la espera pues esto es lo que permite liberar los recursos del procesador para otros hilos y otros programas durante la espera.

Ahora presentamos una extensión de simpanim llamada animador.

Si su navegador reconociese la etiqueta de applet, aquí vería un applet

Este nuevo ejemplo tiene tres novedades. La primera es que es interactivo, es decir, se agregan algunos botones para controlar la animación y poder ver cada imagen por separado. La segunda novedad es que se pueden pasar las imágenes al applet de una manera abreviada usando sólo dos parámetros: BASE y EXT. Esto resulta útil si la aplicación permite cambiar los nombres de las imágenes de manera que tengan una parte fija que es lo que se pasa como parámetro BASE, y el resto del nombre son pares de dígitos entre 00 y 99. La extensión de todas las imágenes debe ser la misma y esto es lo que se indica con el valor del parámetro EXT. Esta forma de leer las imágenes se encuentra en el método getImages(String BASE,String EXT).  Ésta es la llamada al applet animador:

<applet code="unidad06.animador.class" codebase="./"
        width="156" height="140" align="top">
  <param name="PASO" value="250">
  <param name="PAUSA" value="250">
  <param name="BASE" value="images/ws0519">
  <param name="EXT" value="jpg">
</applet>

La tercera novedad está dentro del mismo método  getImages(String BASE,String EXT) y consiste del uso de la clase  java.awt.MediaTracker.   Se trata de una clase que permite interactuar con el proceso de lectura de las imágenes. En el ejemplo creamos un objeto de esta clase pasándole el applet como parámetro del constructor MediaTracker(Component comp). Luego, usando el método addImage(Image ima,int id),   agregamos cada posible imagen al objeto con un identificador numérico diferente para cada una. Esto permite que el "tracker" nos de información sobre la lectura de cada imagen. La llamada al método waitForID(int id) pide al sistema que la imagen con el identificador i sea leida inmediatamente. Finalmente isErrorID(int id) nos informa si hubo algún error en la lectura de la imagen o si ésta se realizó sin contratiempos. Esto es lo que nos permite detectar si una cierta imagen se pudo leer o no y en caso afirmativo la agregamos a las de la animación. El uso del MediaTracker es conveniente para los applets que tienen que leer imágenes de internet y realizar animaciones con ellas, porque el largo tiempo de lectura puede producir que las imágenes aparezcan incompletas y haya parpadeos mientras no se han cargado todas. En realidad debería emplearse este control aún en simpanim; no se hizo solamente para no complicar demasiado el primer ejemplo de la sección.

Ahora veremos un ejemplo de animación simple en la que se usan muchos hilos. Comenzamos por construir una sencilla clase llamada giraAguja que es una pizarra (Canvas) que implementa Runnable. La idea es representar a una aguja (como de un reloj) girando. Éste es el código:

Si su navegador reconociese la etiqueta de applet, aquí vería un applet

En esta clase se ilustra un método muy útil para realizar animaciones sin parpadeos. Consiste en el uso de una imagen artificial sobre la que se dibuja para crear lo que se quiere poner en pantalla antes de exhibirlo. Al final se proyecta esa imagen a la pantalla (en este caso a la pizarra). El alumno deberá comprobar que si en el siguiente ejemplo elimina el uso de la imagen auxiliar auxima y realiza los dibujos directamente sobre la pizarra, se produce un parpadeo muy desagradable a la vista.

Ahora creamos un applet que utiliza 16 de estas agujas giratorias:

Si su navegador reconociese la etiqueta de applet, aquí vería un applet

Haciendo un clic sobre uno de los círculos se puede cambiar la dirección de giro de la aguja.

Este ejemplo muestra que pueden convivir muchos hilos simultáneamente, siempre y cuando cada uno de ellos pase la mayor parte de su vida "durmiendo". El ciclo del método run de una animación debe hacer siempre una llamada al método sleep del hilo, se recomienda un mínimo de 20 milisegundos, pero si puede ser mayor, mejor. El tiempo que debe transcurrir entre la presentación de una imagen y la siguiente para que haya una apariencia de movimiento suave es de entre 40 y 60 mms (milisegundos). Si el tiempo dT que el procesador ocupa en realizar el trabajo de un ciclo es mucho menor que 40 mms entonces conviene poner un "sleep" de 40 milisegundos y así podrán funcionar varias animaciones simultáneas, pero si dT es mayor que 60 mms no se podrá presentar ni una sola animación de manera satisfactoria. Por supuesto dT depende tanto de la complejidad del ciclo como de la velocidad del procesador que se utiliza. Si el programador no incluye un sleep dentro del ciclo de una animación, puede bloquear la computadora de su cliente, incluso con una sola animación.

Ejercicios