Clever asynchronous repaint in Java

≡放荡痞女 提交于 2019-12-07 01:21:33

问题


I have a use-case coming from a GUI problem I would like to submit to your sagacity.

Use case

I have a GUI that displays a computation result depending on some parameters the user set in a GUI. For instance, when the user moves a slider, several events are fired, that all trigger a new computation. When the user adjust the slider value from A to B, a dozens of events are fired.

But the computation can take up to several seconds, whereas the slider adjustment can fire an event every few 100 ms.

How to write a proper Thread that would listen to these events, and kind of filter them so that the repaint of the results is lively? Ideally you would like something like

  • start a new computation as soon as first change event is received;
  • cancel the first computation if a new event is received, and start a new one with the new parameters;
  • but ensure that the last event will not be lost, because the last completed computation needs to be the one with last updated parameters.

What I have tried

A friend of mine (A. Cardona) proposed this low level approach of an Updater thread that prevents too many events to trigger a computation. I copy-paste it here (GPL):

He puts this in a class that extends 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
    ...
}

Every-time an event is sent by the GUI stating that parameters have been changed, we call updater.doUpdate(). This causes the method refresh() to be called much less. But I have no control on this.

Another way?

I was wondering if there is another way to do that, that would use the jaca.concurrent classes. But I could not sort in the Executors framework what would be the one I should start with.

Does any of you have some experience with a similar use case?

Thanks


回答1:


If you're using Swing, the SwingWorker provides capabilities for this, and you don't have to deal with the thread pool yourself.

Fire off a SwingWorker for each request. If a new request comes in and the worker is not done, you can cancel() it, and just start a new SwingWorker. Regarding what the other poster said, I don't think publish() and process() are what you are looking for (although they are also very useful), since they are meant for a case where the worker might fire off events faster than the GUI can process it.

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
    }       
}

Both the action and the done() method are executed on the same thread, so they can effectively check the reference to whether there is an existing worker.

Note that effectively this is doing the same thing that allows a GUI to cancel an existing operation, except the cancel is done automatically when a new request is fired.




回答2:


I would provide a further degree of disconnect between the GUI and the controls by using a queue.

If you use a BlockingQueue between the two processes. Whenever the controls change you can post the new settings to the queue.

Your graphics component can read the queue whenever it likes and act on the arriving events or discard them as necessary.




回答3:


I would look into SwingWorker.publish() (http://docs.oracle.com/javase/6/docs/api/javax/swing/SwingWorker.html)

Publish allows the background thread of a SwingWorker object to cause calls to the process() method, but not every publish() call results in a process() call. If multiple process calls are made before process() returns and can be called again, SwingWorker concatenates the parameters used for multiple publish calls into one call to process.

I had a progress dialog which displayed files being processed; the files were processed faster than the UI could keep up with them, and I didn't want the processing to slow down to display the file names; I used this and had process display only the final filename sent to process(); all I wanted in this case was to indicate to the user where the current processing was, they weren't going to read all the filenames anyway. My UI worked very smoothly with this.




回答4:


Take a look at the implementation of javax.swing.SwingWorker (source code in the Java JDK), with a focus on the handshaking between two methods: publish and process.

These won't be directly applicable, as-is, to your problem - however they demonstrate how you might queue (publish) updates to a worker thread and then service them in your worker thread (process).

Since you only need the last work request, you don't even need a queue for your situation: keep only the last work request. Sample that "last request" over some small period (1 second), to avoid stopping/restarting many many times every 1 second, and if it's changed THEN stop the work and restart.


The reason you don't want to use publish / process as-is is that process always runs on the Swing Event Dispatch Thread - not at all suitable for long running calculations.




回答5:


The key here is that you want to be able to cancel an ongoing computation. The computation must frequently check a condition to see if it needs to abort.

volatile Param newParam;

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

    return result
}

To handover param from event thread to compute thread

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;

And the compute thread does

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


来源:https://stackoverflow.com/questions/15412260/clever-asynchronous-repaint-in-java

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!