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)