views:

69

answers:

2

I have a SwingWorker which calls some code that does not check for thread interruption. After the call to worker.cancel(true), the worker.get() method will throw CancellationException immediately (as it is supposed to). However, since the background task's code never checks for its thread to be interrupted, it happily continues executing.

Is there a standard way to wait for the background task to actually finish? I'm looking to show a "Cancelling..." message or something of the sort and block until the task has terminated. (I'm sure I could always accomplish this with a flag in the worker class if necessary, just looking for any other solutions.)

+1  A: 

The closest thing to a standard, or ready-made way of doing this is the progress property and/or the publish/process method pair provided by SwingWorker. You can set this to a "I'm finished" value at the end of the method to indicate the background work is done. The thread waiting on the swing worker can put up a "Canceling..." message and periodically check the progress to see if it has reached completion. If the waiting thread is the swing EDT, then you will need to use a Timer to periodically check the progress property and clear the cancel message when done.

Here's some example code that runs a stubborn background thread, which is canceled, and then waits until the progress reaches 100.

@Test
public void testSwingWorker()
{
    SwingWorker worker = new SwingWorker() {

        @Override
        protected void process(List chunks)
        {
            for (Object chunk : chunks)
            {
                System.out.println("process: "+chunk.toString());
            }
        }

        @Override
        protected void done()
        {
            System.out.println("done");
        }

        @Override
        protected Object doInBackground() throws Exception
        {
            // simulate long running method
            for (int i=0; i<1000000000; i++)
            {
                double d = Math.sqrt(i);
            }
            System.err.println("finished");
            publish("finished");
            setProgress(100);
            return null;
        }
    };
    Thread t = new Thread(worker);
    t.start();

    try
    {
        worker.get(1, TimeUnit.SECONDS);
    }
    catch (InterruptedException e)        {
    }
    catch (ExecutionException e)        {
    }
    catch (TimeoutException e)        {
    }

    worker.cancel(true);

    // now wait for finish.
    int progress = 0;
    do
    {
        try
        {
            Thread.sleep(1000);
        }
        catch (InterruptedException e)
        {
        }
        progress = worker.getProgress();
        System.out.println(String.format("progress %d", progress));
    }
    while (progress<100);
}

An alternative approach is to use the publish\process method pairs to push a special value indicating that the background thread has finished into the EDT. Your process override method in SwingWorker then picks up this special value and hides the "Canceling..." message. The advantage with this is that no polling or timers are needed. The example code shows that although done is called as soon as the task is canceled, the publish/process method pairs still work even when the task is cancelled.

mdma
Both those ideas are things I hadn't considered yet. I don't like using the progress for the same reason you cited - it requires polling. I like the publish/process idea for the most part, but it couples the logic I want to do when the task is _actually_ finished to the `SwingWorker` class itself. For that same reason I rarely use the `done()` method and usually add listeners to the worker object instead.
Paul Blessing
I agree with you, publish/process is the better of the two. As to coupling, you can decouple it by creating a SwingWorker subclass that overrides the process method, listens for the "I'm done" value, and notify registered listeners that the background task is really done. This separates the logic from the swing worker.
mdma
True, but then how would the listening object block in the meantime until it receives the notification?
Paul Blessing
I was assuming the logic would be executed on the EDT - that's the strength of using publish/process - it hops the published data from the background thread to the EDT. If you need a separate thread to block on completion, then register a listener, which contains the CoundownLatch from your own answer. When notified, the listener trips the countdown latch, allowing the awaiting thread to continue.
mdma
Got it. Since the waiting thread is the EDT and it's presumably blocking because of the "Cancelling..." dialog, the listener you're speaking of would be one that would close the dialog, thus un-blocking the EDT.That said, I'm still not 100% sold on that option, in case the `SwingWorker` I'm using is actually using publish/process for another purpose.
Paul Blessing
A: 

I played around with this a bit and here's what I came up with. I'm using a CountDownLatch and basically exposing its await() method as a method on my SwingWorker object. Still looking for any better solutions.

final class Worker extends SwingWorker<Void, Void> {

    private final CountDownLatch actuallyFinishedLatch = new CountDownLatch(1);

    @Override
    protected Void doInBackground() throws Exception {
        try {
            System.out.println("Long Task Started");

            /* Simulate long running method */
            for (int i = 0; i < 1000000000; i++) {
                double d = Math.sqrt(i);
            }

            return null;
        } finally {
            actuallyFinishedLatch.countDown();
        }
    }

    public void awaitActualCompletion() throws InterruptedException {
        actuallyFinishedLatch.await();
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.execute();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {

        }

        System.out.println("Cancelling");
        worker.cancel(true);

        try {
            worker.get();
        } catch (CancellationException e) {
            System.out.println("CancellationException properly thrown");
        } catch (InterruptedException e) {

        } catch (ExecutionException e) {

        }

        System.out.println("Awaiting Actual Completion");
        try {
            worker.awaitActualCompletion();
            System.out.println("Done");
        } catch (InterruptedException e) {

        }
    }

}
Paul Blessing