lunes, 10 de julio de 2017

Java Swing: SwingWorker y el hilo que despacha los Eventos

Concurrencia en Swing
Cuando programamos aplicaciones con componentes Java Swing, debemos tener mucho cuidado con la concurrencia(ejecutar más de una tarea al mismo tiempo). En Java la concurrencia se implementa mediante Threads o Hilos. Un programa Swing diseñado correctamente es aquel que usa la concurrencia para crear una interfaz de usuario que no se "congele" mientras que el programa ejecuta una tarea que toma mucho tiempo. El programa siempre está disponible para responder a las peticiones del usuario, no importa lo que esté haciendo. Para crear un programa responsive(que reacciona rapidamente y positivamente) el programador debe aprender como el framework Swing emplea los hilos.

Un programador Swing trata con los siguientes tipos de hilos:
  • Hilos iniciales, el hilo que inicia la ejecución del codigo de la aplicación, usualmente llamado main.
  • El hilo que despacha los eventos(EDT), donde se ejecuta todo el código que maneja eventos del usuario y codigo que manipule cualquier componente Swing.
  • Hilos Background(Worker Threads), hilos que ejecutan aquellas tareas que consumen un tiempo considerable(más de un segundo para mi criterio)
El programador no necesita explicitamente codificar la creación de estos hilos: El trabajo del programador es utilizar estos hilos para crear un programa Swing mantenible y responsive.
Como cualquier otro programa corriendo en la plataforma Java, un programa Swing puede adicionalmente crear otros hilos y pools de hilos, siempre y cuando se maneje correctamente la concurrencia.

El fragmento de código del siguiente programa no maneja correctamente la concurrencia

/**
 *
 * @author Roberto Lopez
 */
public class WrongSwingProgram {
 
 public static void main(String args[]){
  final JLabel label = new JLabel();
  
  Thread anyThread = new Thread(new Runnable() {
   @Override
   public void run() {
    label.setText("Hola mundo!!");
    label.setPreferredSize(new Dimension(150, 32));
   }
  });
  
  anyThread.start();
 }
}

El componente Swing label está siendo manipulado por el código que será ejecutado en el hilo anyThread, un hilo que no es el hilo EDT, esto puede generar inconsistencias ya que más de un hilo puede manipular el estado del objeto. Más adelante veremos cómo manejar correctamente la concurrencia en programas Swing.

Hilos Iniciales
Todo programa tiene asociado un conjunto de Hilos que se inician junto con el programa. En un programa estandar solo se tiene un hilo, el hilo que invoca al metodo main de la clase. En applets uno de los hilos inicial es aquel que construye el objeto apple e invoca a los metodos init y start; estas acciones pueden ocurrir en un solo hilo,  o en dos o tres diferentes hilos, dependiendo de la implementación de la plataforma Java. A estos hilos se les llama Hilos Iniciales.

En programas Swing, el hilo inicial no tiene mucho que hacer. La mayor parte del trabajo consiste en crear un objeto Runnable que inicializa la GUI y programa su ejecución en el hilo de eventos EDT, mas adelante explicaré en detalle el hilo de eventos.
Una vez que la GUI esta creada, el progama es principalmente manejado por eventos GUI, cada uno de los cuales ejecutan una tarea corta en el hilo de eventos. Encolar tareas en hilo de eventos sin que necesariamente sean eventos es posible, ya que no existe restricción, sin embargo deben ser tareas que consuman muy poco tiempo, menos de un segundo a mi criterio, así no interferimos con el procesamiento de eventos, para tareas que demanden más de un segundo de tiempo sera necesario usar un hilo worker.

Un hilo inicial encola la tarea que crea la GUI (en el hilo de eventos) invocando a javax.swing.SwingUtilities.invokeLater o javax.swing.SwingUtilities.invokeAndWait. Ambos metodos toman un solo argumento: el objeto Runnable que define la nueva tarea. La unica diferencia la podemos deducir por sus nombres, invoqueLater simplemente encola la tarea en el hilo EDT y regresa; invoqueAndWait espera hasta que la tarea termine y solo cuando esta haya terminado regresa.

Por qué no simplemente es el hilo inicial el encargado de crear la GUI? Esto se debe a que la mayoria del codigo interactua con componentes Swing debe ser ejecutado en el hilo que despacha eventos. Esta restricción será explicada más adelante.

El hilo que despacha eventos EDT
El código que maneja los eventos Swing se ejecuta en un hilo especial conocido como el hilo que despacha los eventos. La mayoría del código que invoca metodos de componentes Swing también se ejecutan en este hilo. Esto es necesario porque la mayoria de los componentes Swing no son "thread safe": acceder a estos metodos desde varios hilos es peligroso, los hilos pueden modificar el estado del objeto uno sobre otro o errores de consistencia de memoria (diferentes hilos tienen una vista inconsistente de lo que debería ser el mismo dato). Todo código que accesa o invocación de metodos de componentes Swing debe ser ejecutado desde el hilo que despacha los eventos. Programas que ignoren esta regla pueden funcionar correctamente la mayor parte del tiempo, pero están sujetos a comportamientos y errores impredecibles que son dificiles de reproducir.

Es útil pensar en los códigos que se ejecutan en el hilo que despacha los eventos como un conjunto de tareas pequeñas. La mayoria de estas tareas son invocaciones de metodos que manejan eventos tales como ActionListener.actionPerformed. Otras tareas pueden ser encoladas en el hilo mediante el código de la aplicación, usando invoqueLater o invoqueAndWait.
Las tareas a ejecutarse en el hilo que despacha eventos debe terminar muy rapidamente, de no ser así se corre el riesgo que el hilo no esté disponible para ejecutar los eventos de la interfaz de usuario.
Si necesitas determinar si tu codigo está siendo ejecutado por el hilo que despacha eventos, invocar a javax.swing.SwingUtilities.isEventDispatchThread.

Worker Threads y SwingWorker
Cuando un programa Swing necesita ejecutar una tarea que consume un tiempo considerable (más de un segundo a mi criterio), generalmente usaremos un WorkerThread, también conocido como un background thread. Cada tarea corriendo en un worker thread es representado por una instancia de javax.swing.SwingWorker. SwingWorker es una clase abstracta; debes crear una subclase para crear tu propio SwingWorker y poder crear instancias; las clases internas son muy usadas para crear objetos SwingWorker simples.

SwingWorker proporciona un conjunto de caracteristicas de control y comunicación:

  • La subclase SwingWorker puede sobreescribir al metodo done, que es automaticamente invocado y ejecutado en el hilo que despacha los eventos, se llama cuando la tarea a ejecutar ha finalizado.
  • SwingWorker implementa a java.util.concurrent.Future. Esta interfaz permite a la tarea que se ejecuta en modo background retornar un valor a otro hilo. Otros metodos en esta interface permite cancelar la tarea y consultar por el estado de la tarea, saber si ha finalizado o fue cancelado.
  • La tarea ejecutándose en background puede entregar resultados del avance de la tarea, invocando a SwingWorker.publish, haciendo que se invoque a SwingWorker.process sobre el hilo que despacha los eventos.
  • La tarea ejecutándose en background puede definir un conjunto propios de propiedades. Cambios sobre estás propiedades disparan eventos, los metodos del manejo de eventos son ejecutados en el hilo que despacha los eventos, el EDT.
Una tarea simple que se ejecuta en modo background (en segundo plano, fuera de la GUI)

Vamos a empezar con a una tarea que es muy simple pero que puede consumir un buen de tiempo en ejecutarse. La clase TumbleItem carga un conjunto de imagenes que se usaran en una animación. Si los archivos de las imagenes son cargados desde un hilo inicial (el hilo main, por ejemplo), pasará un buen tiempo antes que la GUI se muestre. Si los archivos de las imagenes son cargados desde el hilo que despecha los eventos EDT, la GUI quedará congelada temporalmente y no responderá a otros eventos.
Para evitar estos problemas, TumbleItem crea y ejecuta una instancia de SwingWorker desde el hilo inicial. El metodo doInBackground, ejecutandose en un WorkerThread, carga las imagenes dentro un arreglo de ImageIcon, y devuelve una referencia del arreglo. Entonces, el metodo done se ejecuta en el Hilo que despacha los eventos, para recuperar la referencia al arreglo invocamos el metodo get. Cargar las imagenes en modo background permitirá construir y visualizar inmediamente la GUI, sin tener que esperar a que la carga de imagenes termine.

NOTA: Algunas clases y porciones de clases son propiedad de Oracle

Aquí el código que define y ejecuta el objeto SwingWorker.


SwingWorker worker = new SwingWorker<ImageIcon[], Void>() {
    @Override
    public ImageIcon[] doInBackground() {
        final ImageIcon[] innerImgs = new ImageIcon[nimgs];
        for (int i = 0; i < nimgs; i++) {
            innerImgs[i] = loadImage(i+1);
        }
        return innerImgs;
    }

    @Override
    public void done() {
        //Remove the "Loading images" label.
        animator.removeAll();
        loopslot = -1;
        try {
            imgs = get();
        } catch (InterruptedException ignore) {}
        catch (java.util.concurrent.ExecutionException e) {
            String why = null;
            Throwable cause = e.getCause();
            if (cause != null) {
                why = cause.getMessage();
            } else {
                why = e.getMessage();
            }
            System.err.println("Error retrieving file: " + why);
        }
    }
};



El poyecto completo lo puedes descargar desde mi repositorio en GitHub

Toda clase concreta que extienda de SwingWorker debe implementar el metodo doInBackground; la implementación del metodo done es opcional.
Observa que SwingWorker es una clase generica, con dos tipos de parametros. El primer tipo de parametro especifica el tipo que retornará el metodo doInBackground, y también para el metodo get, que es invocado por otro hilo que recibe el objeto retornado por doInBackground. El segundo tipo de parametro de SwingWorker especifica un tipo para resultados preliminares que son retornados mientras la tarea en background todavía está activa.
Para este ejemplo no retornamos resultados preliminares así que especificamos el tipo Void, mas adelante sacaré una nueva versión con resultados preliminares.
Es posible que te preguntes si el codigo que obtiene el arreglo de iconos imgs es innecesariamente complicado. ¿Por qué hacemos que doInBackground retorne un objeto y usamos el metodo done para recuperarlo? ¿Por qué no solo obtenemos el arreglo imgs directamente del metodo doInBackground? El problema es que el objeto imgs  es creado en el hilo worker y usado en el hilo que despacha los eventos. Cuando los objetos son compartidos entre hilos siguiendo esta forma, nos aseguramos que los cambios hechos en un hilo son visibles en los otros. Usando get lo garantizamos, porque usando get creamos una relación happens before(relación entre el resultado de dos eventos, tal que uno de estos eventos debe ocurrir antes que otro evento) entre el codigo que crea el arreglo imgs y el codigo que lo usa.
Actualmente hay dos formas de recuperar el objeto retornado por doInBackground.


  • Invocando SwingWorker.get sin argumentos. Si la tarea en background no ha finalizado, get bloquea el hilo hasta que termine la tarea.
  • Invocando SwingWorker.get con argumentos indicando un time-out. Si la tarea en background no ha finalizado, el metodo get bloquea el hilo hasta que termine la tarea - al menos que el time-out expire primero, en ese caso get lanza una Excepción java.util.concurrent.TimeoutException.

Hay que tener cuidado cuando se invoque al metodo get desde el hilo que despacha los eventos. Hasta que get retorne, la GUI no podrá procesar más eventos, y la GUI quedará "congelada". No invoques el metodo get sin argumentos al menos que estes completamente seguro de que la tarea en background ha finalizado completamente.

Referencia
https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html

1 comentario: