¿Por qué es responsabilidad de la persona que llama garantizar la seguridad del hilo en la programación de la GUI?

37

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:

  1. ¿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?
  2. ¿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.

durron597
fuente
1
Eh, rendimiento en mi humilde opinión. Esas publicaciones de eventos no son gratuitas, y en aplicaciones no triviales querrás minimizar su número (y asegurarte de que ninguna sea demasiado grande), pero esa minimización / condensación debería realizarse lógicamente en el presentador.
Ordous
1
@Ordous Todavía puede asegurarse de que las publicaciones sean mínimas al colocar la transferencia de hilos en la vista.
durron597
2
Hace algún tiempo leí un blog realmente bueno que analiza este tema, 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, dependiendo de cómo se implemente, las condiciones de carrera en el marco de referencia. 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.
MadProgrammer
1
SWT hace cumplir el concepto de seguridad de subprocesos lanzando excepciones si lo viola, no bonito, pero al menos se lo ha informado. Menciona el cambio de Swing a JavaFX, pero tendría este problema con casi cualquier marco de IU, 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 se sincronicen 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
MadProgrammer,
1
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 quiere resolver este problema, según sus necesidades
MadProgrammer

Respuestas:

22

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.

Karl Bielefeldt
fuente
25

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.

monstruo de trinquete
fuente
1
Curiosamente, resuelvo este problema teniendo un marco de colas para estas actualizaciones que escribí que gestiona la transferencia de hilos.
durron597
2
@ durron597: ¿Y nunca tiene actualizaciones dependiendo del estado actual de la interfaz de usuario que otro hilo pueda influir? Entonces podría funcionar.
Deduplicador
¿Por qué necesitarías cerraduras anidadas? ¿Por qué necesita bloquear toda la ventana raíz cuando trabaja en detalles en una ventana secundaria? El orden de bloqueo es un problema solucionable al proporcionar un método expuesto único para bloquear múltiples bloqueos en el orden correcto (ya sea de arriba hacia abajo o de abajo hacia arriba, pero no deje la opción a la persona que llama)
MSalters
1
@MSalters con root Me refería a la ventana actual. Para obtener todos los bloqueos que necesita adquirir, debe subir la jerarquía que requiere bloquear cada contenedor a medida que lo encuentra, obtener el padre y desbloquearlo (para asegurarse de que solo bloquee de arriba hacia abajo) y luego esperar que no haya cambiado en después de obtener la ventana raíz y hacer el bloqueo de arriba hacia abajo.
Ratchet Freak
@ratchetfreak: si el niño que intenta bloquear es eliminado por otro hilo mientras lo está bloqueando, eso es un poco desafortunado pero no relevante para el bloqueo. Simplemente no puede operar en un objeto que otro hilo acaba de eliminar. Pero, ¿por qué ese otro hilo elimina objetos / ventanas que su hilo todavía está usando? Eso no es bueno en ningún escenario, no solo en la interfaz de usuario.
MSalters
17

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 Setde tipo: mientras que Contains(..)y Add(...)y Update(...)es un API perfectamente válido en un solo escenario roscado, el escenario multi-roscado necesita una AddOrUpdate.

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.

  1. El kit de herramientas no puede resolver ese problema ya que el bloqueo no garantiza que el orden de las operaciones se mantenga correcto.
  2. La vista puede resolver el problema, pero solo si permite que la regla comercial establezca que el número en la parte superior de la lista debe coincidir con el número de elementos de la lista y solo actualiza la lista a través de la vista. No es exactamente lo que se supone que es MVC.
  3. El presentador puede resolverlo, pero debe tener en cuenta que la vista tiene necesidades especiales con respecto a los subprocesos.
  4. La vinculación de datos a un modelo compatible con subprocesos múltiples es otra opción. Pero eso complica el modelo con cosas que deberían ser preocupaciones de UI.

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.

Patricio
fuente
Tal vez podría introducirse una capa entre la Vista y el Presentador que también podría intercambiarse.
durron597
2
.NET tiene System.Windows.Threading.Dispatcherque 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.
Patrick
9

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.

MadProgrammer
fuente
2
No tendrías este problema con la web. JavaScript no tiene soporte para subprocesos deliberadamente: un tiempo de ejecución de JavaScript es efectivamente solo una gran cola de eventos. (Sí, sé que, estrictamente hablando, JS tiene WebWorkers: estos son hilos nerfed y se comportan más como actores en otros idiomas).
James_pic
1
@James_pic Lo que realmente tiene es la parte del navegador que actúa como sincronizador de la cola de eventos, que es básicamente de lo que hemos estado hablando, la persona que llama ha sido responsable de garantizar que se produzcan actualizaciones con la cola de eventos de los kits de herramientas
MadProgrammer
Sí exactamente. La diferencia clave, en el contexto de la web, es que esta sincronización ocurre independientemente del código de la persona que llama, porque el tiempo de ejecución no proporciona ningún mecanismo que permita la ejecución del código fuera de la cola de eventos. Por lo tanto, la persona que llama no necesita hacerse responsable de ello. Creo que esa es también una gran parte de la motivación detrás del desarrollo de NodeJS: si el entorno de tiempo de ejecución es un bucle de eventos, todo el código es consciente del bucle de eventos de forma predeterminada.
James_pic
1
Dicho esto, hay marcos de interfaz de usuario del navegador que tienen su propio evento-loop-dentro de un evento-loop (te estoy mirando angular). Debido a que estos marcos ya no están protegidos por el tiempo de ejecución, las personas que llaman también deben asegurarse de que el código se ejecute dentro del bucle de eventos, de forma similar al código multiproceso en otros marcos.
James_pic
¿Habría algún problema particular con tener controles de "solo visualización" que proporcionen seguridad de subprocesos automáticamente? Si uno tiene un control de texto editable que está escrito por un hilo y leído por otro, no hay una buena manera de hacer que la escritura sea visible para el hilo sin sincronizar al menos una de esas acciones con el estado real de la interfaz de usuario del control, pero, ¿importarían estos problemas para los controles de solo visualización? Creo que esos podrían proporcionar fácilmente cualquier bloqueo o enclavamiento necesarios para las operaciones realizadas en ellos, y permitir que la persona que llama ignore el momento de cuando ...
supercat