在process()调用完成之前执行SwingWorker,done()

时间:2014-07-25 15:04:39

标签: java swing swingworker

我和SwingWorker一起工作了一段时间,并且最终有一种奇怪的行为,至少对我而言。我清楚地知道,由于性能原因,在publish()方法中的几次调用在一次调用中得到了充分利用。这对我来说非常有意义,我怀疑SwingWorker会保留某种队列来处理所有调用。

根据tutorial和API,当SwingWorker结束执行时,doInBackground()正常结束或工作线程从外部取消,然后调用done()方法。到目前为止一切都很好。

但我有一个示例(类似于教程中所示),执行 process()方法后, 完成done()方法调用。由于两个方法都在Event Dispatch Thread中执行,我希望在所有done()调用完成后执行process()。换句话说:

预期:

Writing...
Writing...
Stopped!

结果:

Writing...
Stopped!
Writing...

示例代码

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();
            }
        });
    }
}

2 个答案:

答案 0 :(得分:17)

简短回答:

这是因为publish()没有直接调度process,它设置了一个定时器,它将在DELAY之后触发EDT中process()块的调度。因此,当工作人员被取消时,仍然有一个计时器等待调度process()与上次发布的数据。使用计时器的原因是实现优化,其中可以使用多个发布的组合数据执行单个进程。

LONG ANSWER:

让我们看看publish()和cancel如何相互交互,为此,让我们深入研究一些源代码。

首先是简单的部分,cancel(true)

public final boolean cancel(boolean mayInterruptIfRunning) {
    return future.cancel(mayInterruptIfRunning);
}

此取消最终会调用以下代码:

boolean innerCancel(boolean mayInterruptIfRunning) {
    for (;;) {
        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;
}

SwingWorker状态设置为CANCELLED,线程被中断并且done()被调用,但这不是SwingWorker的完成,而是future done(),在SwingWorker构造函数中实例化变量时指定:

future = new FutureTask<T>(callable) {
    @Override
    protected void done() {
        doneEDT();  // <-----
        setState(StateValue.DONE);
    }
};

doneEDT()代码是:

private void doneEDT() {
    Runnable doDone =
        new Runnable() {
            public void run() {
                done(); // <-----
            }
        };
    if (SwingUtilities.isEventDispatchThread()) {
        doDone.run(); // <-----
    } else {
        doSubmit.add(doDone);
    }
}

如果我们在EDT中,那么直接调用SwingWorkers的done()是我们的情况。此时SwingWorker应该停止,不应再调用publish(),这很容易通过以下修改来演示:

while(!isCancelled()) {
    textArea.append("Calling publish\n");
    publish("Writing...\n");
}

然而,我们仍然得到一个&#34;写作......&#34;来自process()的消息。那么让我们看看process()是如何调用的。 publish(...)的源代码是

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);  // <-----
}

我们发现Runnable run()的{​​{1}}最终会调用doProcess,但此代码只调用process(args)而非doProcess.add(chunks)并且&# 39; sa doProcess.run()也在附近。我们来看doSubmit

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 is what will make that for multiple publishes only one process is executed submit(); // <----- } } 实际上做的是将块添加到一些内部ArrayList publish()并调用arguments。我们刚看到提交只调用submit(),这是一个非常相同的doSubmit.add(this)方法,因为adddoProcess都延伸doSubmit,但这一次{ {1}} AccumulativeRunnable<V>代替V,而不是Runnable。所以一个块是可以运行的String。但是,doProcess调用是process(args)类中定义的完全不同的方法:

submit()

它创建一个Timer,在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) { for (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(); // <----- } } 代码一次。一旦事件被触发,代码将在EDT中排队,这将调用内部actionPerformed,最终调用DELAY的{​​{1}},从而执行run(),其中chunk是run(flush()) ArrayList的刷新数据。我跳过了一些细节,&#34; run&#34;电话是这样的:

  • doSubmit.run()
  • doSubmit.run(flush())//实际上是一个runnables循环,但只有一个(*)
  • doProcess.run()
  • doProcess.run(flush())
  • 处理(块)

(*)布尔值doProcessprocess(chunk)(重置此布尔值)使得发布的附加调用不会添加要在doSubmit.run中调用的doProcess runnable(flush( ))但是他们的数据不会被忽略。因此,对于在计时器生命期间调用的任何数量的发布执行单个进程。

总而言之,arguments的作用是在 DELAY之后安排在EDT 中调用isSubmited。这就解释了为什么即使在我们取消了线程并且没有更多的发布完成之后,仍然会出现一个流程执行,因为我们取消工作的那一刻(很有可能)会安排flush()的计时器publish("Writing...")已经安排好后。

为什么使用此计时器而不是仅使用process(chunk)在EDT中调度进程()?要实现docs中解释的性能优化:

  

因为在Event上异步调用了process方法   Dispatch Thread可能会发生多次调用   在执行过程方法之前。出于性能目的   这些调用合并为一个带有连接的调用   参数。       例如:

process()

我们现在知道这是有效的,因为在DELAY间隔内发生的所有发布都将他们的done()添加到我们看到invokeLater(doProcess)的内部变量中, publish("1"); publish("2", "3"); publish("4", "5", "6"); might result in: process("1", "2", "3", "4", "5", "6") 将执行所有这些数据一气呵成。

这是一个BUG吗?替代方法吗

很难说如果这是一个错误,处理后台线程已发布的数据可能是有意义的,因为工作已经完成,您可能有兴趣更新GUI尽可能多的信息(例如,args正在做什么)。然后,如果arguments需要处理所有数据和/或在done()创建数据/ GUI不一致之后调用process(),则可能没有意义。

如果您不希望在完成()之后执行任何新进程(),那么有一个明显的解决方法,只需检查工作是否也在process(chunk)方法中被取消! / p>

process()

在最后一个进程()之后执行done()会更加棘手,例如,完成后也可以使用一个计时器,它将在&gt; DELAY之后调度实际的done()工作。虽然我不能认为这是一个常见的情况,因为如果你取消了当我们知道我们实际上取消了所有未来的进程时,错过一个进程()并不重要

答案 1 :(得分:0)

阅读了DSquare的精湛答案,并从中得出结论,需要进行一些子类化,我已经为任何需要确保在EDT中处理了所有已发布块的人提出了这个想法。继续前进。

NB我试图用Java而不是Jython(我选择的语言和官方世界上最好的语言)来编写它,但它有点复杂,例如,publish是{{1}因此,您必须创建另一种方法来调用它,还因为您必须(哈欠)使用Java中的泛型对所有内容进行参数化。

这个代码应该是任何Java人都可以理解的:只是为了帮助final,当结果为0时,这将评估为self.publication_counter.get()

False

因此,在您的调用代码中,您输入的内容如下:

# this is how you say Worker... is a subclass of SwingWorker in Python/Jython
class WorkerAbleToWaitForPublicationToFinish( javax.swing.SwingWorker ):

    # __init__ is the constructor method in Python/Jython
    def __init__( self ):

        # we just add an "attribute" (here, publication_counter) to the object being created (self) to create a field of the new object
        self.publication_counter = java.util.concurrent.atomic.AtomicInteger()

    def await_processing_of_all_chunks( self ):
        while self.publication_counter.get():
            time.sleep( 0.001 )

    # fully functional override of the Java method     
    def process( self, chunks ):
        for chunk in chunks:
            pass
            # DO SOMETHING WITH EACH CHUNK

        # decrement the counter by the number of chunks received
        # NB do this AFTER dealing with the chunks 
        self.publication_counter.addAndGet( - len( chunks ) )

    # fully functional override of the Java method     
    def publish( self, *chunks ):
        # increment the counter by the number of chunks received
        # NB do this BEFORE publishing the chunks
        self.publication_counter.addAndGet( len( chunks ))
        self.super__publish( chunks )

PS使用像这样的 engine.update_xliff_task.get() engine.update_xliff_task.await_processing_of_all_chunks() 子句(即轮询技术)并不优雅。我查看了可用的while类,例如java.util.concurrentCountDownLatch(两者都使用了线程阻塞方法),但我认为不适合这个目的......

<强>

我对此感兴趣的是调整一个名为Phaser的适当的并发类(用Java编写,在Apache站点上找到)。如果达到CounterLatch计数器的值,则他们的版本会停止await() 处的帖子。我的版本允许你要么做到这一点,要么相反:说出&#34;等待直到计数器达到某个值然后再解锁闩锁&#34;:

NB AtomicLong使用AtomicLongsignal使用AtomicBoolean:因为在原始Java中,他们使用released关键字。我认为使用原子类将达到同样的目的。

volatile

所以我的代码现在看起来像这样:

在SwingWorker构造函数中:

class CounterLatch():
    def __init__( self, initial = 0, wait_value = 0, lift_on_reached = True ):
        self.count = java.util.concurrent.atomic.AtomicLong( initial )
        self.signal = java.util.concurrent.atomic.AtomicLong( wait_value )

        class Sync( java.util.concurrent.locks.AbstractQueuedSynchronizer ):
            def tryAcquireShared( sync_self, arg ):
                if lift_on_reached:
                    return -1 if (( not self.released.get() ) and self.count.get() != self.signal.get() ) else 1
                else:
                    return -1 if (( not self.released.get() ) and self.count.get() == self.signal.get() ) else 1
            def tryReleaseShared( self, args ):
                return True

        self.sync = Sync()
        self.released = java.util.concurrent.atomic.AtomicBoolean() # initialised at False

    def await( self, *args ):
        if args:
            assert len( args ) == 2
            assert type( args[ 0 ] ) is int
            timeout = args[ 0 ]
            assert type( args[ 1 ] ) is java.util.concurrent.TimeUnit
            unit = args[ 1 ]
            return self.sync.tryAcquireSharedNanos(1, unit.toNanos(timeout))
        else:
            self.sync.acquireSharedInterruptibly( 1 )

    def count_relative( self, n ):
        previous = self.count.addAndGet( n )
        if previous == self.signal.get():
            self.sync.releaseShared( 0 )
        return previous

在SW.publish:

self.publication_counter_latch = CounterLatch() 

在等待块处理停止的线程中:

self.publication_counter_latch.count_relative( len( chunks ) )
self.super__publish( chunks )