聪明的Java异步重绘

7

我有一个GUI问题的使用案例想提交给您的智慧。

使用案例

我有一个GUI,它显示根据用户在GUI中设置的一些参数计算结果。例如,当用户移动滑块时,会触发多个事件,所有这些事件都会触发新计算。当用户将滑块值从A调整到B时,会触发几十个事件。

但是计算可能需要几秒钟,而滑块调整可以每隔几百毫秒触发一次事件。

如何编写一个适当的线程来监听这些事件,并对其进行过滤,以便结果的重绘是生动的?理想情况下,您希望像这样:

  • 收到第一个更改事件后立即启动新计算;
  • 如果收到新事件,则取消第一个计算,并使用新参数启动新计算;
  • 但确保不会丢失最后一个事件,因为最后一个完成的计算需要是具有最新更新参数的计算。

我尝试过的方法

我的一个朋友(A. Cardona)提出了这种低级方法,即Updater线程,可以防止太多事件触发计算。我在此处复制粘贴它(GPL):

他将其放在一个扩展线程的类中:

public void doUpdate() {
    if (isInterrupted())
        return;
    synchronized (this) {
        request++;
        notify();
    }
}

public void quit() {
    interrupt();
    synchronized (this) {
        notify();
    }
}

 public void run() {
    while (!isInterrupted()) {
        try {
            final long r;
            synchronized (this) {
                r = request;
            }
            // Call refreshable update from this thread
            if (r > 0)
                refresh(); // Will trigger re-computation
            synchronized (this) {
                if (r == request) {
                    request = 0; // reset
                    wait();
                }
                // else loop through to update again
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


public void refresh() {
    // Execute computation and paint it
    ...
}

每当GUI发送事件说明参数已更改时,我们调用updater.doUpdate()。 这会导致refresh()方法被调用的次数少得多。 但我对此没有控制。 还有其他方法吗? 我想知道是否有另一种方法来做到这一点,可以使用jaca.concurrent类。 但是,我无法在Executors框架中排序应该从哪一个开始。 你们中有没有人有类似用例的经验?
谢谢
5个回答

4
如果你正在使用 Swing,SwingWorker 可以提供此功能,你不必自己处理线程池。
对于每个请求,启动一个 SwingWorker。如果有新的请求并且 worker 尚未完成,则可以使用 cancel() 方法取消它,并启动一个新的 SwingWorker。关于另一个帖子中说的,我认为 publish() 和 process() 不是你要找的(尽管它们也非常有用),因为它们是为工作程序可能比 GUI 更快地触发事件的情况而设计的。
ThingyWorker worker;

public void actionPerformed(ActionEvent e) {
    if( worker != null ) worker.cancel();
    worker = new ThingyWorker();
    worker.execute();
}

class ThingyWorker extends SwingWorker<YOURCLASS, Object> {
    @Override protected YOURCLASS doInBackground() throws Exception {
        return doSomeComputation(); // Should be interruptible
    }   
    @Override protected void done() {
        worker = null; // Reset the reference to worker

        YOURCLASS data;

        try {
            data = get();
        } catch (Exception e) { 
            // May be InterruptedException or ExecutionException                
            e.printStackTrace();
            return;
        }           

        // Do something with data
    }       
}

动作和done()方法都在同一线程上执行,因此它们可以有效地检查是否存在现有的工作线程引用。

请注意,实际上这做的是与允许GUI取消现有操作相同的事情,只是当新请求被触发时自动执行取消操作。


好的解决方案。但是我不确定在worker1.cancel()之后是否仍有小概率执行worker1.done(),并且它可能会在worker2.done()之后执行,即旧结果会覆盖新结果。这里需要进行更多的调查。 - ZhongYu
这并没有解决如何批量处理请求的问题,以避免在短时间内多次取消和重新启动线程的情况。 - Richard Sitze
我认为你永远无法避免取消请求,因为你永远不知道某个请求是否是最后一个。当然,你可以在触发工作程序之前等待一段时间,以查看是否有更多的请求到来,但这只会使更新变慢。 - Andrew Mao
如果可能的话,@zhong.j.yu,似乎非常不可能,因为worker2.doInBackground()将比worker1.cancel()花费更长时间。另外,由于cancel()execute()done()都在事件分派线程中,所以如果Swing同步正确,cancel()应该防止调用done()。这里的取消没有竞争条件。 - Andrew Mao

1
我会使用队列来进一步隔离GUI和控件之间的联系。如果在两个进程之间使用BlockingQueue,则每当控件更改时,您可以将新设置发布到队列中。您的图形组件可以随时读取队列并根据需要处理到达的事件或丢弃它们。

1
我会看一下SwingWorker.publish() (http://docs.oracle.com/javase/6/docs/api/javax/swing/SwingWorker.html)。 publish允许SwingWorker对象的后台线程调用process()方法,但并非每个publish()调用都会导致process()调用。如果在process()返回之前进行了多个process调用并且可以再次调用,则SwingWorker将多个publish调用使用的参数连接成一个process调用。
我有一个显示正在处理文件的进度对话框;文件的处理速度比UI跟上它们的速度更快,我不想减慢处理速度来显示文件名;我使用了这个,并且只让process显示最终发送到process()的文件名;在这种情况下,我想要的只是向用户指示当前处理位置,他们不会阅读所有的文件名。我的UI使用这个非常流畅。

谢谢!它适用于除GUI之外的其他东西吗? - Jean-Yves
1
我认为processpublish不是OP正在寻找的。您无法使用新事件更新SwingWorker。在这种情况下,问题是不同的:处理速度比UI触发的操作慢。 - Andrew Mao
@AndrewMao 很有道理--发布/处理正在更新UI,这个问题包括从UI更新处理。当然,UI生成的事件已经排队了;也许程序可以接收滑块事件,直到队列中没有更多事件为止,然后开始处理最近的值,因为早期的值已不再相关。因此,我认为我的想法与更新UI相关,但您说得对,还需要单独考虑UI到处理的问题。 - arcy
1
@Jean-Yves 我对使用SwingWorker除了UI之外的任何方式都不熟悉,虽然我不知道它是否可以实现。我想到了另一种方法:让滑块启动一个计时器;如果计时器已经在运行,则更新其超时时间。当计时器到期时,开始处理。这样,只有最后一个排队的滑块事件会被处理。 - arcy
@Jean-Yves,还可以查看http://docs.oracle.com/javase/tutorial/uiswing/components/slider.html了解有关滑块可监听的更改事件的信息;它提供了一个仅对“最后”更改感兴趣的示例,即不关心在滑动到最终值时发生的所有事件,而是在滑动停止时感兴趣于该值。 - arcy

1
请查看javax.swing.SwingWorker的实现(Java JDK中的源代码),关注两个方法之间的握手:publishprocess
这些不会直接适用于您的问题 - 然而,它们演示了如何将更新排队(发布)到工作线程,然后在您的工作线程中服务它们(处理)。
由于您只需要最后一个工作请求,因此您甚至不需要为您的情况使用队列:仅保留最后一个工作请求。在一段小时间内(1秒钟)抽样“最后一个请求”,以避免每1秒钟停止/重启许多次,并且如果已更改,则停止工作并重新启动。
你不想直接使用publish / process的原因是process总是在Swing事件分派线程上运行,不适合长时间运行的计算。

这取决于他如何使用它——如果所有的过程都是更新UI,那么它就是他想要的。我同意他不必(也不应该)在那里进行长时间的计算。但它恰恰适合他问题的一部分。 - arcy

0
关键在于你希望能够取消正在进行的计算。计算必须经常检查条件,以确定是否需要中止。
volatile Param newParam;

Result compute(Param param)
{
    loop
        compute a small sub problem
        if(newParam!=null) // abort
            return null;  

    return result
}

将参数从事件线程传递到计算线程。
synchronized void put(Param param)  // invoked by event thread
    newParam = param;
    notify();

synchronized Param take()
    while(newParam==null)
        wait();
    Param param = newParam;
    newParam=null;
    return param;

计算线程执行

public void run()
    while(true)
        Param param = take();
        Result result = compute(param);
        if(result!=null)
            paint result in event thread

我不知道“频繁检查条件”(即轮询)是什么,这就是中断的作用。 - Andrew Mao
你如何中断一个忙碌的循环?:) - ZhongYu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接