sábado, 30 de abril de 2016

Java Swing: Event Dispatch Thread (EDT) - Parte 2

En este artículo continuo hablando sobre el Event Dispatch Thread de Java Swing del que comencé a hablar en el artículo anterior.

Aunque este artículo pueda dar una impresión muy negativa del uso de Swing, no es mi intención criticar negativamente dicho proyecto ni asustar a quienes estén usándolo, sino tan sólo comentar características que, a mi jucio, deben tenerse muy en cuenta al usarlo para no llevarse sorpresas desagradables y que en la mayoría de la documentación a penas se comentan.

Y en cualquier caso, hay que tener en cuenta que muchas de las reflexiones de este artículos son extrapolables a otros entornos, como por ejemplo, el UI Thread de Android.

Restricción impuesta por el Event Dispatch Thread


Una cuestión clave sobre el funcionamiento de Swing es que todo el código que maneje/acceda a componentes debe ejecutarse dentro el EDT (ver Swing's Threading Policy), lo cual no es ninguna tontería. Los componentes no son Thread safe, es decir, no incluyen código de sincronización para que sean accesibles de forma consistente desde varios hilos.

Eso se debe, fundamentalmente, a razones de eficiencia y complejidad de desarrollo. Por una parte, habría muchísimo overhead (coste extra) computacional provocado por el código de sincronización, que influiría negativamente en la fluidez de la GUI. Y por otra parte, conseguir que la GUI fuese accesible desde varios hilos de forma completamente consistente sería una tarea extremadamente compleja (en este artículo uno de sus creadores reflexiona sobre el tema Multithreaded toolkits: A failed dream?).

Complicaciones


A priori la restricción no parece incómoda, de hecho, en muchas ocasiones los programadores ni nos planteamos el uso de varios hilos. Sin embargo, hay que ser conscientes de que en cuanto se utiliza algún componente para la GUI se usan, de forma implícita, al menos 2 hilos: el principal y el EDT.

En el artículo anterior expliqué lo fácil que es bloquear la GUI realizando alguna tarea costosa (acceso a disco, acceso a red, cálculos muy complejos, etc) dentro del EDT. Para evitar que la GUI se bloquee habrá que realizar las tareas pesadas fuera del EDT, pero sin acceder a los componentes de la GUI desde fuera de él. En otras palabras, gestionar asíncronamente los eventos.

Por ejemplo, al pulsar un botón se podría realizar una consulta sobre una base de datos y rellenar un JTable con los datos obtenidos:
  • El evento de pulsación del botón se recibirá dentro del EDT.
  • El JTable ya podría estar creado y mostrándose vacío.
  • La consulta a la base de datos habrá que hacerla fuera del EDT, desde otro hilo.
  • Para poder rellenar el JTable, habrá que esperar a tener los resultados.
  • Habrá que construir un TableModel con los resultados, ¿dentro o fuera del EDT?
  • Habrá que asignar el nuevo TableModel al JTable, dentro del EDT.

O sea, que habrá que andar "saltando" de un hilo a otro dependiendo de la tarea. ¿Y qué pasa con la GUI mientras se obtienen los datos de la consulta?
  • ¿se bloquea la GUI?
  • ¿se ignora la interacción del usuario (descartando clics de ratón y pulsaciones de teclado)?
  • ¿se muestra algún indicador de que la aplicación está ocupada (como el típico cursor de ratón con un reloj de arena)?
  • ¿se muestra un indicador del progreso de la tarea?
  • ¿la GUI sigue siendo operativa? ¿y si se vuelve a pulsar en el botón de cargar mientras hay una o varias cargas en proceso?

Obviamente, el código resultante ya no es tan sencillo como poner dentro del oyente del botón una tras otra las instrucciones:
Además, implementar alguna de las alternativas para la transición es un problema en sí mismo.

La opción de establecer un modelo nuevo completo no siempre es viable, ya que se pueden tener instalados oyentes interesados en saber cuando se añaden o quitan elementos. Estos oyentes deben ser notificados dentro el EDT. ¿Y si añadir o quitar elementos es una tarea pesada que deba realizarse fuera del EDT? ¿Y si hay sucesos que ocurren fuera del EDT que afecten a la GUI?

Por ejemplo, supóngase una aplicación que observa cierta tabla de una base de datos, mostrando su contenido mediante un JTable con su correspondiente TableModel. En el servidor se añade una nueva fila a la tabla y se notifica a la aplicación observadora. Esa notificación debería recibirse desde fuera del EDT. Habrá que actualizar el TableModel del JTable o asignarle uno nuevo dentro el EDT.

Más y más complicaciones


Al gestionar eventos de forma asíncrona se complica la gestión de eventos interrelacionados, como por ejemplo un MOUSE_PRESSED y el siguiente MOUSE_RELEASED. Al recibir la notificación del primer evento dentro del EDT y delegar su gestión a otro hilo, el EDT recibirá el siguiente evento, mientras es muy probable que aún se esté procesando el anterior: ¿y ahora, se delega el segundo evento a un tercer hilo, se descarta, se memoriza en algún sitio hasta que se pueda procesar?

También puede ocurrir que en medio de una tarea pesada sea necesario consultar al usuario, por ejemplo para que confirme si realizar o no una operación peligrosa, o para que introduzca algún dato necesario. Lo normal es que la tarea pesada se esté ejecutando fuera del EDT, por lo que habrá que pausarla para consultar al usuario dentro del EDT y luego continuarla fuera del EDT (leyendo la respuesta que se obtuvo dentro del EDT).

Aparte de la complicación de tener gestionar los eventos de forma asíncrona, al programar en varios hilos pueden aparecer los problemas intrínsecos de la programación concurrente: coordinar (sincronizar) los diversos hilos.

Otro problema añadido que aparece es el trocear el código del algoritmo creando objetos (típicamente anónimos) Runnable con el código a ejecutar dentro del EDT y con objetos SwingWorker (la opción típica) para ejecutar código fuera del EDT. En el caso de ser objetos de clases locales anónimas, además hay que tener en cuenta que no se puede acceder a variables locales del método envolvente a menos que dichas variables hayan sido declaradas como final.

Cuando se acopla fuertemente la vista con el modelo, se tiende a reducir código creándolos de manera conjunta. Por ejemplo, Swing lo hace con el constructor JTable(Object[][] rowData, Object[] columnNames), en el que se construye internamente un TableModel a partir de esos arrays. Inspirados en él, se podría tener la idea de crear una subclase con el constructor MyTable(Connection, Query) que realice una consulta sobre una base de datos y construya el JTable con las filas resultantes (o una función factoría JTable showQuery(Connection, Query) equivalente). El new MyTable se haría:
  • ¿dentro del EDT ejecutando una consulta a base de datos desde él, y por tanto, bloqueándolo?
  • ¿fuera del EDT violando la norma de manipular los objetos de GUI siempre dentro del EDT?
Ninguna de las 2 opciones es completamente buena.

Más vale prevenir


Resumiendo todo lo anterior, la gestión de los eventos se puede complicar mucho.

La cuestión es tener todo eso en cuenta, saber que todo eso está detrás para tener en cuenta que cuando algún evento pueda derivar en una tarea pesada (como entrada/salida) más vale entretenerse en orientarlo a un diseño asíncrono desde el principio, a tener que cambiarlo a posteriori.

Por experiencia sé que convertir código síncrono monohilo en código asíncrono multihilo con las restricciones comentadas en el artículo no es una tarea sencilla, y suele requerir modificar código en muchos más sitios de los que sería deseable.

Enlaces


Los enlaces de interés relacionados con esta serie de artículos sobre el EDT están aquí, en el primero de los artículos.


(Actualizado 30/04/2016)

No hay comentarios:

Publicar un comentario