2009-11-16 11 views
16

Como es ampliamente conocido, todo lo relacionado con los componentes Swing se debe hacer en the event dispatch thread. Esto también se aplica al models detrás de los componentes, como TableModel. Lo suficientemente fácil en casos básicos, pero las cosas se vuelven bastante complicadas si el modelo es una "vista en vivo" de algo que debe ejecutarse en un hilo separado porque está cambiando rápidamente. Por ejemplo, una vista en vivo de un mercado de valores en una JTable. Los mercados de valores no suelen ocurrir en el EDT.¿Cómo sincronizar el modelo Swing con un modelo "real" que cambia rápidamente?

Entonces, ¿cuál es el patrón preferible para (des) acoplar el modelo Swing que debe estar en el EDT, y un modelo "real", seguro para subprocesos que debe ser actualizable desde cualquier lugar, en cualquier momento? Una posible solución sería realmente split the model en dos copias separadas: el modelo "real" más su contraparte Swing, que es una instantánea del modelo "real". Luego están (bidireccionalmente) sincronizados en el EDT de vez en cuando. Pero esto se siente como hinchazón. ¿Este es realmente el único enfoque viable, o hay otras formas más estándar? Bibliotecas útiles? ¿Cualquier cosa?

+1

productor/consumidor? –

Respuesta

11

puedo recomendar el siguiente enfoque:

  • eventos lugar que debe modificar la tabla en una cola "en espera de evento", y cuando un evento se coloca en la cola y la cola está vacía luego invoque el hilo Despacho de evento para drenar la cola de todos los eventos y actualizar el modelo de tabla. Esta optimización significa ya no está invocando el hilo de envío de eventos para cada evento recibido, que resuelve el problema de que la cadena de distribución de eventos no se corresponde con la secuencia de eventos subyacente.
  • Evite la creación de un Runnable nuevo al invocar el hilo de envío de eventos mediante el uso de una clase interna sin estado para drenar la cola de eventos pendientes dentro de la implementación de su panel de tabla.
  • Opcional optimización adicional: al drenar la cola de eventos pendientes minimice el número de eventos de actualización de tabla al recordar qué filas de la tabla deben repintarse y luego disparar un evento único (o un evento por fila) después de procesar todos los eventos.

Ejemplo Código

public class MyStockPanel extends JPanel { 
    private final BlockingQueue<StockEvent> stockEvents; 

    // Runnable invoked on event dispatch thread and responsible for applying any 
    // pending events to the table model. 
    private final Runnable processEventsRunnable = new Runnable() { 
    public void run() { 
     StockEvent evt; 

     while ((evt = stockEvents.poll() != null) { 
     // Update table model and fire table event. 
     // Could optimise here by firing a single table changed event 
     // when the queue is empty if processing a large #events. 
     } 
    } 
    } 

    // Called by thread other than event dispatch thread. Adds event to 
    // "pending" queue ready to be processed. 
    public void addStockEvent(StockEvent evt) { 
    stockEvents.add(evt); 

    // Optimisation 1: Only invoke EDT if the queue was previously empty before 
    // adding this event. If the size is 0 at this point then the EDT must have 
    // already been active and removed the event from the queue, and if the size 
    // is > 0 we know that the EDT must have already been invoked in a previous 
    // method call but not yet drained the queue (i.e. so no need to invoke it 
    // again). 
    if (stockEvents.size() == 1) { 
     // Optimisation 2: Do not create a new Runnable each time but use a stateless 
     // inner class to drain the queue and update the table model. 
     SwingUtilities.invokeLater(processEventsRunnable); 
    } 
    } 
} 
+0

Gracias, definitivamente parece que estuviste allí :-) –

+0

Por cierto, ese código tiene un error sutil: si alguien agrega un StockEvent justo cuando la cola está a punto de estar vacía, stockEvents.isEmpty() puede devolver falso pero el processEventsRunnable finaliza antes de que los nuevos eventos se agreguen a la cola stockEvents. Entonces está en estado atascado. Una solución podría ser usar otro mecanismo distinto de isEmpty() para verificar si se debe relanzar processEventsRunnable. –

+1

@Joonas: ese es un buen punto. Creo que el problema se resuelve al verificar ese tamaño() == 1 después de agregar el evento a la cola. Si la cola está vacía en este momento, entonces el evento ya se ha procesado, por lo que no hay problemas allí, y si el tamaño es> 1, entonces sabemos que el EDT ya debe haber sido invocado por una llamada al método anterior. – Adamski

2

Por lo que tengo entendido, no desea implementar interfaces de modelo Swing en su modelo real, ¿o sí? ¿Se puede implementar un modelo Swing como una "vista" sobre una parte de un modelo real? Traduirá su acceso de lectura getValueAt() a las llamadas del modelo real, y el modelo real notificará al modelo Swing sobre los cambios, ya sea proporcionando una lista de cambios o asumiendo que el modelo Swing se encargará de consultar los nuevos valores de todo lo que se muestra actualmente.

+0

Derecha: No quiero implementar interfaces Swing en mi modelo real, porque sé que el modelo real se actualizará a partir de hilos distintos al EDT, lo que violaría los requisitos de Swing. Creo que lo que describes es esencialmente el "modelo dividido". –

+0

No es un gran problema actualizar el modelo real de otros subprocesos, siempre que la interacción con Swing ocurra en EDT y su modelo sea seguro para subprocesos. Swing llamará a métodos como getValueAT() en EDT, por lo que lo único de lo que preocuparse es enviar notificaciones (fireSmthChanged) en EDT. Puede hacerlo usando SwingUtilities.invokeLater – Dmitry

+0

... pero, por supuesto, enfrentará problemas sutiles, como el número incorrecto de hijos en un árbol (porque algo ha cambiado desde que envió una notificación a Swing), etc. – Dmitry

2

El enfoque habitual es enviar "señales" de algún tipo a las que la interfaz de usuario escucha. En mi código, a menudo uso un despachador central que envía señales que contienen el objeto que se modificó, el nombre del campo/propiedad más el valor antiguo y nuevo. No se envía ninguna señal para el caso oldValue.equals(newValue) o oldValue.compareTo(newValue) == 0 (este último para las fechas y BigDecimal).

El hilo de la interfaz de usuario luego registra un oyente para todas las señales. A continuación, examina el objeto y el nombre y luego lo traduce a un cambio en la interfaz de usuario que se ejecuta a través de asyncExec().

Puede convertirlo en un detector por objeto y hacer que cada elemento de la UI se registre en el modelo. Pero descubrí que esto solo propaga el código por todos lados. Cuando tengo un gran conjunto de objetos en ambos lados, a veces solo uso varias señales (o eventos) para hacer las cosas más manejables.

+0

Ok, entonces tiene un modelo dividido y luego envía notificaciones de actualización del modelo "real" al modelo de la interfaz de usuario. 'asyncExec()' parece ser una función SWT. ¿Es esencialmente similar a 'EventQueue.invokeLater()'? –

+0

Sí, use 'EventQueue.invokeLater()' :) Mi enfoque es mantener las dependencias entre los dos modelos lo más delgada posible, así que puedo cambiar fácilmente cualquiera. Siempre que las señales no cambien (mucho), el otro modelo no se verá afectado. –

Cuestiones relacionadas