java - SwingWorker, done() is executed before process() calls are finished -
i have been working swingworkers while , have ended strange behavior, @ least me. understand due performance reasons several invocations publish() method coallesced in 1 invocation. makes sense me , suspect swingworker keeps kind of queue process calls.
according tutorial , api, when swingworker ends execution, either doinbackground() finishes or worker thread cancelled outside, done() method invoked. far good.
but have example (similar shown in tutorials) there process()
method calls done after done()
method executed. since both methods execute in event dispatch thread expect done()
executed after process()
invocations finished. in other words:
expected:
writing... writing... stopped!
result:
writing... stopped! writing...
sample code
import java.awt.borderlayout; import java.awt.dimension; import java.awt.graphics; import java.awt.event.actionevent; import java.util.list; import javax.swing.abstractaction; import javax.swing.action; import javax.swing.jbutton; import javax.swing.jframe; import javax.swing.jpanel; import javax.swing.jscrollpane; import javax.swing.jtextarea; import javax.swing.swingutilities; import javax.swing.swingworker; public class demo { private swingworker<void, string> worker; private jtextarea textarea; private action startaction, stopaction; private void createandshowgui() { startaction = new abstractaction("start writing") { @override public void actionperformed(actionevent e) { demo.this.startwriting(); this.setenabled(false); stopaction.setenabled(true); } }; stopaction = new abstractaction("stop writing") { @override public void actionperformed(actionevent e) { demo.this.stopwriting(); this.setenabled(false); startaction.setenabled(true); } }; jpanel buttonspanel = new jpanel(); buttonspanel.add(new jbutton(startaction)); buttonspanel.add(new jbutton(stopaction)); textarea = new jtextarea(30, 50); jscrollpane scrollpane = new jscrollpane(textarea); jframe frame = new jframe("test"); frame.setdefaultcloseoperation(jframe.dispose_on_close); frame.add(scrollpane); frame.add(buttonspanel, borderlayout.south); frame.pack(); frame.setlocationrelativeto(null); frame.setvisible(true); } private void startwriting() { stopwriting(); worker = new swingworker<void, string>() { @override protected void doinbackground() throws exception { while(!iscancelled()) { publish("writing...\n"); } return null; } @override protected void process(list<string> chunks) { string string = chunks.get(chunks.size() - 1); textarea.append(string); } @override protected void done() { textarea.append("stopped!\n"); } }; worker.execute(); } private void stopwriting() { if(worker != null && !worker.iscancelled()) { worker.cancel(true); } } public static void main(string[] args) { swingutilities.invokelater(new runnable() { @override public void run() { new demo().createandshowgui(); } }); } }
short answer:
this happens because publish() doesn't directly schedule process
, sets timer fire scheduling of process() block in edt after delay
. when worker cancelled there still timer waiting schedule process() data of last publish. reason using timer implement optimization single process may executed combined data of several publishes.
long answer:
let's see how publish() , cancel interact each other, that, let dive source code.
first easy part, cancel(true)
:
public final boolean cancel(boolean mayinterruptifrunning) { return future.cancel(mayinterruptifrunning); }
this cancel ends calling following code:
boolean innercancel(boolean mayinterruptifrunning) { (;;) { int s = getstate(); if (ranorcancelled(s)) return false; if (compareandsetstate(s, cancelled)) // <----- break; } if (mayinterruptifrunning) { thread r = runner; if (r != null) r.interrupt(); // <----- } releaseshared(0); done(); // <----- return true; }
the swingworker state set cancelled
, thread interrupted , done()
called, not swingworker's done, future
done(), specified when variable instantiated in swingworker constructor:
future = new futuretask<t>(callable) { @override protected void done() { doneedt(); // <----- setstate(statevalue.done); } };
and doneedt()
code is:
private void doneedt() { runnable dodone = new runnable() { public void run() { done(); // <----- } }; if (swingutilities.iseventdispatchthread()) { dodone.run(); // <----- } else { dosubmit.add(dodone); } }
which calls swingworkers's done()
directly if in edt our case. @ point swingworker should stop, no more publish()
should called, easy enough demonstrate following modification:
while(!iscancelled()) { textarea.append("calling publish\n"); publish("writing...\n"); }
however still "writing..." message process(). let see how process() called. source code publish(...)
is
protected final void publish(v... chunks) { synchronized (this) { if (doprocess == null) { doprocess = new accumulativerunnable<v>() { @override public void run(list<v> args) { process(args); // <----- } @override protected void submit() { dosubmit.add(this); // <----- } }; } } doprocess.add(chunks); // <----- }
we see run()
of runnable doprocess
ends calling process(args)
, code calls doprocess.add(chunks)
not doprocess.run()
, there's dosubmit
around too. let's see doprocess.add(chunks)
.
public final synchronized void add(t... args) { boolean issubmitted = true; if (arguments == null) { issubmitted = false; arguments = new arraylist<t>(); } collections.addall(arguments, args); // <----- if (!issubmitted) { //this make multiple publishes 1 process executed submit(); // <----- } }
so publish()
adding chunks internal arraylist arguments
, calling submit()
. saw submit calls dosubmit.add(this)
, same add
method, since both doprocess
, dosubmit
extend accumulativerunnable<v>
, time around v
runnable
instead of string
in doprocess
. chunk runnable calls process(args)
. submit()
call different method defined in class of dosubmit
:
private static class dosubmitaccumulativerunnable extends accumulativerunnable<runnable> implements actionlistener { private final static int delay = (int) (1000 / 30); @override protected void run(list<runnable> args) { (runnable runnable : args) { runnable.run(); } } @override protected void submit() { timer timer = new timer(delay, this); // <----- timer.setrepeats(false); timer.start(); } public void actionperformed(actionevent event) { run(); // <----- } }
it creates timer fires actionperformed
code once after delay
miliseconds. once event fired code enqueued in edt call internal run()
ends calling run(flush())
of doprocess
, executing process(chunk)
, chunk flushed data of arguments
arraylist. skipped details, chain of "run" calls this:
- dosubmit.run()
- dosubmit.run(flush()) //actually loop of runnables have 1 (*)
- doprocess.run()
- doprocess.run(flush())
- process(chunk)
(*)the boolean issubmited
, flush()
(which resets boolean) make additional calls publish don't add doprocess runnables called in dosubmit.run(flush()) data not ignored. executing single process amount of publishes called during life of timer.
all in all, publish("writing...")
scheduling call process(chunk)
in edt after delay. explains why after cancelled thread , no more publishes done, still 1 process execution appears, because moment cancel worker there's (with high probability) timer schedule process()
after done()
scheduled.
why timer used instead of scheduling process() in edt invokelater(doprocess)
? implement performance optimization explained in docs:
because process method invoked asynchronously on event dispatch thread multiple invocations publish method might occur before process method executed. performance purposes these invocations coalesced 1 invocation concatenated arguments. example:
publish("1"); publish("2", "3"); publish("4", "5", "6"); might result in: process("1", "2", "3", "4", "5", "6")
we know works because publishes occur within delay interval adding args
internal variable saw arguments
, process(chunk)
execute data in 1 go.
is bug? workaround?
it's hard tell if bug or not, might make sense process data background thread has published, since work done , might interested in getting gui updated info can (if that's process()
doing, example). , might not make sense if done()
requires have data processed and/or call process() after done() creates data/gui inconsistencies.
there's obvious workaround if don't want new process() executed after done(), check if worker cancelled in process
method too!
@override protected void process(list<string> chunks) { if (iscancelled()) return; string string = chunks.get(chunks.size() - 1); textarea.append(string); }
it's more tricky make done() executed after last process(), example done use timer schedule actual done() work after >delay. although can't think common case since if cancelled shouldn't important miss 1 more process() when know in fact cancelling execution of future ones.
Comments
Post a Comment