【问题标题】:Clever asynchronous repaint in JavaJava 中巧妙的异步重绘
【发布时间】:2013-03-02 23:28:54
【问题描述】:

我有一个来自 GUI 问题的用例,我想提交给您。

用例

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

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

如何编写一个合适的线程来监听这些事件,并过滤它们以使结果的重绘更加生动?理想情况下,您会喜欢类似的东西

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

我的尝试

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

他把它放在一个扩展 Thread 的类中:

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 框架中排序我应该从哪个开始。

你们中的任何人都有类似用例的经验吗?

谢谢

【问题讨论】:

    标签: java multithreading java.util.concurrent


    【解决方案1】:

    如果您使用SwingSwingWorker 提供了这方面的功能,您不必自己处理线程池。

    为每个请求触发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
        }       
    }
    

    action和done()方法都在同一个线程上执行,所以可以有效的检查引用是否存在worker。

    请注意,这实际上与允许 GUI 取消现有操作的操作相同,但取消是在触发新请求时自动完成。

    【讨论】:

    • 很好的解决方案。但我不清楚在worker1.cancel() 之后,worker1.done() 是否仍然执行的可能性很小,并且它可以在worker2.done() 之后执行,即较旧的结果会覆盖较新的结果。这里需要更多调查。
    • 这并没有解决如何批量请求的问题,以避免在短时间内多次取消和重新启动线程的情况。
    • 我认为您永远无法避免取消请求,因为您永远不知道某个请求是否是最后一个请求。当然,您可以在解雇工作人员之前稍等片刻,看看是否有更多请求到来,但这只会让更新变慢。
    • @zhong.j.yu 如果可能的话,似乎不太可能,因为worker2.doInBackground() 将比worker1.cancel() 花费更长的时间。此外,由于cancel()execute()done() 都在事件调度线程中,如果Swing 正确同步,似乎cancel() 应该防止done() 被调用。这里的取消没有竞争条件。
    【解决方案2】:

    我将通过使用队列在 GUI 和控件之间提供更大程度的断开连接。

    如果您在两个进程之间使用BlockingQueue。每当控件更改时,您都可以将新设置发布到队列中。

    您的图形组件可以随时读取队列并对到达的事件采取行动或根据需要丢弃它们。

    【讨论】:

      【解决方案3】:

      我会调查 SwingWorker.publish() (http://docs.oracle.com/javase/6/docs/api/javax/swing/SwingWorker.html)

      Publish 允许 SwingWorker 对象的后台线程调用 process() 方法,但并非每个 publish() 调用都会导致 process() 调用。如果在 process() 返回之前进行了多个进程调用并且可以再次调用,SwingWorker 会将用于多个发布调用的参数连接到一个进程调用中。

      我有一个进度对话框,显示正在处理的文件;文件的处理速度比 UI 跟得上它们的速度快,而且我不希望处理速度变慢以显示文件名;我使用了这个并且进程只显示发送到进程()的最终文件名;在这种情况下,我想要的只是向用户指示当前处理的位置,他们无论如何都不会读取所有文件名。我的 UI 在这方面工作得非常顺利。

      【讨论】:

      • 谢谢!它是否适用于 GUI 以外的其他东西?
      • 我不认为 processpublish 是 OP 正在寻找的。您无法使用新事件更新 SwingWorker。在这种情况下,问题就不同了:处理速度比 UI 触发的操作慢。
      • @AndrewMao 好点——由于处理,发布/进程正在更新 UI,这个问题包括从 UI 更新处理。当然,UI 生成的事件已经排队;也许程序可以接收滑块事件,直到队列中没有更多事件,然后开始处理最近的值,因为较早的值不再相关。所以我认为我的想法与更新 UI 相关,但你说得对,需要单独考虑 UI 到处理的问题。
      • @Jean-Yves 我不熟悉将 SwingWorker 用于 UI 以外的任何东西,尽管我不知道它不能完成。我想到了另一种方法:让滑块启动一个计时器;如果计时器已经在运行,请更新其超时时间。当计时器到期时,开始处理。这样,只有一系列排队的滑块事件中的最后一个被处理。
      • @Jean-Yves 还可以查看docs.oracle.com/javase/tutorial/uiswing/components/slider.html 以获取有关您可以从滑块收听的更改事件的信息;它有一个示例,仅对所做的“最后”更改感兴趣,即对滑动到最终值时发生的所有事件不感兴趣,但对滑动停止时的值感兴趣。
      【解决方案4】:

      看一下javax.swing.SwingWorker的实现(Java JDK中的源代码), 重点介绍两种方法之间的握手:publishprocess

      这些不会直接适用于您的问题 - 但它们演示了您如何将更新排队(发布)到工作线程,然后在工作线程(进程)中为它们提供服务。

      由于您只需要最后一个工作请求,因此您甚至不需要针对您的情况排队:只保留最后一个工作请求。在一小段时间(1 秒)内对“最后一个请求”进行采样,以避免每 1 秒多次停止/重新启动,如果更改了,则停止工作并重新启动。


      您不想按原样使用 publish / process 的原因是 process 总是在 Swing Event Dispatch Thread 上运行- 根本不适合长时间运行的计算。

      【讨论】:

      • 这取决于他如何使用它——如果所有的过程都是更新用户界面,那正是他想要的。我同意,他不必(也不应该)在那里进行长时间的计算。但这对于他的部分问题是完全正确的。
      【解决方案5】:

      这里的关键是您希望能够取消正在进行的计算。计算必须经常检查一个条件以查看它是否需要中止。

      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
      

      【讨论】:

      • 我不知道“经常检查一个条件”,也就是轮询;这就是中断的用途。
      • 如何中断一个繁忙的循环:?)
      猜你喜欢
      • 1970-01-01
      • 2020-12-10
      • 2016-10-13
      • 1970-01-01
      • 2014-08-17
      • 1970-01-01
      • 2015-03-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多