He visto, en muchos lugares, que es sabiduría canónica 1 que es responsabilidad de la persona que llama asegurarse de estar en el hilo de la interfaz de usuario al actualizar los componentes de la interfaz de usuario (específicamente, en Java Swing, que está en el hilo de envío de eventos ) .
¿Por qué esto es tan? El hilo de envío de eventos es una preocupación de la vista en MVC / MVP / MVVM; para manejarlo en cualquier lugar, pero la vista crea un acoplamiento estrecho entre la implementación de la vista y el modelo de subprocesos de la implementación de esa vista.
Específicamente, digamos que tengo una aplicación de arquitectura MVC que usa Swing. Si la persona que llama es responsable de actualizar los componentes en el subproceso de envío de eventos, entonces si trato de cambiar mi implementación Swing View por una implementación JavaFX, debo cambiar todo el código del presentador / controlador para utilizar el hilo de la aplicación JavaFX .
Entonces, supongo que tengo dos preguntas:
- ¿Por qué es responsabilidad de la persona que llama garantizar la seguridad del subproceso del componente de la interfaz de usuario? ¿Dónde está la falla en mi razonamiento anterior?
- ¿Cómo puedo diseñar mi aplicación para que tenga un acoplamiento flexible de estos problemas de seguridad de los hilos y, al mismo tiempo, ser adecuadamente seguro para los hilos?
Permítanme agregar un código MCVE Java para ilustrar lo que quiero decir con "responsable de la llamada" (aquí hay algunas otras buenas prácticas que no estoy haciendo, pero estoy tratando de ser lo más mínimo posible):
La persona que llama es responsable:
public class Presenter {
private final View;
void updateViewWithNewData(final Data data) {
EventQueue.invokeLater(new Runnable() {
public void run() {
view.setData(data);
}
});
}
}
public class View {
void setData(Data data) {
component.setText(data.getMessage());
}
}
Ver ser responsable:
public class Presenter {
private final View;
void updateViewWithNewData(final Data data) {
view.setData(data);
}
}
public class View {
void setData(Data data) {
EventQueue.invokeLater(new Runnable() {
public void run() {
component.setText(data.getMessage());
}
});
}
}
1: El autor de esa publicación tiene la puntuación más alta en Swing on Stack Overflow. Dice esto por todas partes y también he visto que es responsabilidad de la persona que llama en otros lugares.
fuente
Respuestas:
Hacia el final de su ensayo de sueño fallido , Graham Hamilton (uno de los principales arquitectos de Java) menciona que si los desarrolladores "deben preservar la equivalencia con un modelo de cola de eventos, deberán seguir varias reglas no obvias" y tener una visión visible y explícita El modelo de cola de eventos "parece ayudar a las personas a seguir el modelo de manera más confiable y, por lo tanto, a construir programas GUI que funcionen de manera confiable".
En otras palabras, si intenta colocar una fachada multiproceso en la parte superior de un modelo de cola de eventos, la abstracción ocasionalmente se filtrará de maneras no obvias que son extremadamente difíciles de depurar. Parece que funcionará en papel, pero termina desmoronándose en la producción.
Agregar pequeños contenedores alrededor de componentes individuales probablemente no será problemático, como actualizar una barra de progreso desde un hilo de trabajo. Si intenta hacer algo más complejo que requiere bloqueos múltiples, comienza a ser realmente difícil razonar sobre cómo interactúan la capa multiproceso y la capa de cola de eventos.
Tenga en cuenta que este tipo de problemas son universales para todos los kits de herramientas GUI. Presumir que un modelo de despacho de eventos en su presentador / controlador no lo está estrechamente acoplando a un solo modelo de concurrencia del kit de herramientas GUI específico, lo está acoplando a todos ellos . La interfaz de colas de eventos no debería ser tan difícil de abstraer.
fuente
Porque hacer que el hilo de la interfaz gráfica de usuario de la GUI sea seguro es un gran dolor de cabeza y un cuello de botella.
El flujo de control en las GUI a menudo va en 2 direcciones desde la cola de eventos a la ventana raíz a los widgets gui y desde el código de la aplicación al widget propagado hasta la ventana raíz.
Diseñar una estrategia de bloqueo que no bloquee la ventana raíz (causará mucha contención) es difícil . Bloquear de abajo hacia arriba mientras otro hilo se bloquea de arriba hacia abajo es una excelente manera de lograr un punto muerto instantáneo.
Y verificar si el hilo actual es el hilo de la GUI cada vez es costoso y puede causar confusión sobre lo que realmente está sucediendo con la GUI, especialmente cuando está haciendo una secuencia de lectura, actualización y escritura. Eso necesita tener un bloqueo en los datos para evitar carreras.
fuente
El enhebrado (en un modelo de memoria compartida) es una propiedad que tiende a desafiar los esfuerzos de abstracción. Un ejemplo sencillo es una
Set
de tipo: mientras queContains(..)
yAdd(...)
yUpdate(...)
es un API perfectamente válido en un solo escenario roscado, el escenario multi-roscado necesita unaAddOrUpdate
.Lo mismo se aplica a la interfaz de usuario: si desea mostrar una lista de elementos con un recuento de los elementos en la parte superior de la lista, debe actualizar ambos en cada cambio.
Ninguno de esos se ve realmente tentador. Se recomienda que el presentador sea responsable del manejo de subprocesos no porque sea bueno, sino porque funciona y las alternativas son peores.
fuente
System.Windows.Threading.Dispatcher
que manejar el despacho a WPF y WinForms UI-Threads. Ese tipo de capa entre el presentador y la vista definitivamente es útil. Pero solo proporciona independencia en el juego de herramientas, no enhebrar la independencia.Leí un blog realmente bueno hace algún tiempo que analiza este tema (mencionado por Karl Bielefeldt), lo que básicamente dice es que es muy peligroso intentar hacer que el kit de interfaz de usuario sea seguro, ya que introduce posibles puntos muertos y depende de cómo sea implementado, las condiciones de carrera en el marco.
También hay una consideración de rendimiento. No tanto ahora, pero cuando Swing se lanzó por primera vez, fue muy criticado por su rendimiento (ha sido malo), pero eso no fue realmente culpa de Swing, fue la falta de conocimiento de la gente sobre cómo usarlo.
SWT hace cumplir el concepto de seguridad de subprocesos lanzando excepciones si lo viola, no bonito, pero al menos se lo ha informado.
Si observa el proceso de pintura, por ejemplo, el orden en que se pintan los elementos es muy importante. No desea que la pintura de un componente tenga un efecto secundario en ninguna otra parte de la pantalla. Imagine que si pudiera actualizar la propiedad de texto de una etiqueta, pero fue pintada por dos hilos diferentes, podría terminar con un resultado dañado. Por lo tanto, toda la pintura se realiza dentro de un solo hilo, normalmente según el orden de requisitos / solicitudes (pero a veces se condensa para reducir el número de ciclos de pintura física reales)
Menciona el cambio de Swing a JavaFX, pero tendría este problema con casi cualquier marco de interfaz de usuario (no solo clientes gruesos, sino también web), Swing parece ser el que resalta el problema.
Podría diseñar una capa intermedia (¿un controlador de un controlador?) Cuyo trabajo es garantizar que las llamadas a la IU estén sincronizadas correctamente. Es imposible saber exactamente cómo podría diseñar sus partes de su API que no son de UI desde la perspectiva de la API de UI y la mayoría de los desarrolladores se quejarían de que cualquier protección de subprocesos implementada en la API de UI era restrictiva o no satisfacía sus necesidades. Es mejor permitirle decidir cómo desea resolver este problema, según sus necesidades
Una de las cuestiones más importantes que debe tener en cuenta es la capacidad de justificar un orden determinado de eventos, en función de las entradas conocidas. Por ejemplo, si el usuario cambia el tamaño de la ventana, el modelo de cola de eventos garantiza que se producirá un orden determinado de eventos, esto puede parecer simple, pero si la cola permite que otros hilos activen eventos, entonces ya no puede garantizar el orden en cuáles son los eventos que pueden ocurrir (una condición de carrera) y, de repente, debes comenzar a preocuparte por diferentes estados y no hacer una cosa hasta que ocurra algo más y comiences a tener que compartir banderas de estado y termines con espagueti.
De acuerdo, podría resolver esto teniendo algún tipo de cola que ordenara los eventos según el momento en que se emitieron, pero ¿no es eso lo que ya tenemos? Además, aún no podría garantizar que el hilo B generaría sus eventos DESPUÉS del hilo A
La razón principal por la que las personas se molestan por tener que pensar en su código es porque están hechas para pensar en su código / diseño. "¿Por qué no puede ser más simple?" No puede ser más simple, porque no es un problema simple.
Recuerdo cuando se lanzó la PS3 y Sony habló sobre el procesador Cell y su capacidad de realizar líneas separadas de lógica, decodificar audio, video, cargar y resolver datos del modelo. Un desarrollador de juegos preguntó: "Eso es increíble, pero ¿cómo sincronizas las transmisiones?"
El problema del que hablaba el desarrollador era, en algún momento, que todas esas transmisiones separadas tendrían que sincronizarse en una sola tubería para la salida. El pobre presentador simplemente se encogió de hombros, ya que no era un problema con el que estaban familiarizados. Obviamente, han tenido soluciones para resolver este problema ahora, pero es divertido en ese momento.
Las computadoras modernas están recibiendo una gran cantidad de información de muchos lugares diferentes al mismo tiempo, toda esa información debe ser procesada y entregada al usuario de inmediato, lo que no interfiere con la presentación de otra información, por lo que es un problema complejo, sin Una única solución simple.
Ahora, tener la capacidad de cambiar los marcos, no es algo fácil de diseñar, PERO, tomar MVC por un momento, MVC puede tener varias capas, es decir, podría tener un MVC que se ocupe directamente de administrar el marco de la interfaz de usuario, usted podría envolver eso, nuevamente, en un MVC de capa superior que se ocupa de interacciones con otros marcos (potencialmente multiproceso), sería responsabilidad de esta capa determinar cómo se notifica / actualiza la capa MVC inferior.
Luego usaría la codificación para interconectar patrones de diseño y patrones de fábrica o de construcción para construir estas diferentes capas. Esto significa que los marcos multiproceso se desacoplan de la capa de interfaz de usuario mediante el uso de una capa intermedia, como idea.
fuente