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

Popular posts from this blog

javascript - jQuery: Add class depending on URL in the best way -

caching - How to check if a url path exists in the service worker cache -

Redirect to a HTTPS version using .htaccess -