在process()调用完成之前执行SwingWorker,done() [英] SwingWorker, done() is executed before process() calls are finished

查看:111
本文介绍了在process()调用完成之前执行SwingWorker,done()的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在使用 SwingWorker s一段时间,最后有一种奇怪的行为,至少对我而言。我清楚地了解,由于性能原因,对 publish()方法在一次调用中被运用。这对我来说非常有意义,我怀疑SwingWorker会保留某种队列来处理所有调用。

I have been working with SwingWorkers for a while and have ended up with a strange behavior, at least for me. I clearly understand that due to performance reasons several invocations to publish() method are coallesced in one invocation. It makes perfectly sense to me and I suspect SwingWorker keeps some kind of queue to process all that calls.

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

According to tutorial and API, when SwingWorker ends its execution, either doInBackground() finishes normally or worker thread is cancelled from the outside, then done() method is invoked. So far so good.

但我有一个例子(类似于教程中所示),其中有 process()方法调用 完成后执行()方法已执行。由于两种方法都在事件调度线程中执行,我希望 done()在所有 process()调用完成后执行。换句话说:

But I have an example (similar to shown in tutorials) where there are process() method calls done after done() method is executed. Since both methods execute in the Event Dispatch Thread I would expect done() be executed after all process() invocations are finished. In other words:

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


推荐答案

简短回答:

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

This happens because publish() doesn't directly schedule process, it sets a timer which will fire the scheduling of a process() block in the EDT after DELAY. So when the worker is cancelled there is still a timer waiting to schedule a process() with the data of the last publish. The reason for using a timer is to implement the optimization where a single process may be executed with the combined data of several publishes.

长期答案:

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

Let's see how publish() and cancel interact with each other, for that, let us dive into some source code.

首先是简单部分,取消(true)

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

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

This cancel ends up calling the following code:

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构造函数中实例化变量时指定:

The SwingWorker state is set to CANCELLED, the thread is interrupted and done() is called, however this is not SwingWorker's done, but the future done(), which is specified when the variable is instantiated in the SwingWorker constructor:

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

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

Which calls the SwingWorkers's done() directly if we are in the EDT which is our case. At this point the SwingWorker should stop, no more publish() should be called, this is easy enough to demonstrate with the following modification:

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

但是我们仍然会从进程()获得写入...消息。那么让我们看看process()是如何调用的。 发布(...)的源代码是

However we still get a "Writing..." message from process(). So let us see how is process() called. The source code for 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);  // <-----
}

我们看到 run()的Runnable doProcess 是谁最终调用进程(args),但是这段代码只调用 doProcess.add(块)而不是 doProcess.run()并且有一个 doSubmit 也是。让我们看看 doProcess.add(块)

We see that the run() of the Runnable doProcess is who ends up calling process(args), but this code just calls doProcess.add(chunks) not doProcess.run() and there's a 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 is what will make that for multiple publishes only one process is executed
        submit(); // <-----
    }
}

那又怎样 publish()实际上是将块添加到一些内部ArrayList 参数并调用 submit( )。我们刚看到提交只调用 doSubmit.add(this),这是非常相同的 add 方法,因为两者都是 doProcess doSubmit extend AccumulativeRunnable< V> ,但是这个时间 V Runnable 而不是 String ,如 doProcess 。所以一个块是runnable,它调用 process(args)。但是 submit()调用是在 doSubmit 类中定义的完全不同的方法:

So what publish() actually does is adding the chunks into some internal ArrayList arguments and calling submit(). We just saw that submit just calls doSubmit.add(this), which is this very same add method, since both doProcess and doSubmit extend AccumulativeRunnable<V>, however this time around V is Runnable instead of String as in doProcess. So a chunk is the runnable that calls process(args). However the submit() call is a completely different method defined in the 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) {
        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(); // <-----
    }
}

它创建在 DELAY 毫秒之后触发 actionPerformed 代码的计时器。一旦事件被触发,代码将在EDT中排队,这将调用内部 run(),最终调用 run(flush()) doProcess 然后执行进程(块),其中chunk是刷新的数据参数 ArrayList。我跳过了一些细节,运行调用链是这样的:

It creates a Timer that fires the actionPerformed code once after DELAY miliseconds. Once the event is fired the code will be enqueued in the EDT which will call an internal run() which ends up calling run(flush()) of doProcess and thus executing process(chunk), where chunk is the flushed data of the arguments ArrayList. I skipped some details, the chain of "run" calls is like this:


  • doSubmit.run()

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

  • doProcess.run()

  • doProcess.run(flush())

  • process(chunk)

  • doSubmit.run()
  • doSubmit.run(flush()) //Actually a loop of runnables but will only have one (*)
  • doProcess.run()
  • doProcess.run(flush())
  • process(chunk)

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

(*)The boolean isSubmited and flush() (which resets this boolean) make it so additional calls to publish don't add doProcess runnables to be called in doSubmit.run(flush()) however their data is not ignored. Thus executing a single process for any amount of publishes called during the life of a Timer.

总而言之,发布(写... 。确实是在 DELAY之后在EDT 中安排对进程(块)的调用。这就解释了为什么即使我们取消了线程并且没有更多的发布完成,仍然会出现一个进程执行,因为我们取消工作的那一刻(很有可能)一个Timer将安排一个进程() 之后已经安排。

All in all, what publish("Writing...") does is scheduling the call to process(chunk) in the EDT after a DELAY. This explains why even after we cancelled the thread and no more publishes are done, still one process execution appears, because the moment we cancel the worker there's (with high probability) a Timer that will schedule a process() after done() is already scheduled.

为什么要使用此Timer而不是只是使用 invokeLater(doProcess)在EDT中调度process()?要实现 docs

Why is this Timer used instead of just scheduling process() in the EDT with an invokeLater(doProcess)? To implement the performance optimization explained in the docs:


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

Because the process method is invoked asynchronously on the Event Dispatch Thread multiple invocations to the publish method might occur before the process method is executed. For performance purposes all these invocations are coalesced into one invocation with concatenated arguments. For example:

 publish("1");
 publish("2", "3");
 publish("4", "5", "6");

might result in:
 process("1", "2", "3", "4", "5", "6")


我们现在知道这是有效的,因为在DELAY间隔内发生的所有发布都在添加 args 进入该内部变量我们看到 arguments 进程(块)将一次性执行所有数据。

We now know that this works because all the publishes that occur within a DELAY interval are adding their args into that internal variable we saw arguments and the process(chunk) will execute with all that data in one go.

这是一个错误吗?解决方法?

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

It's hard to tell If this is a bug or not, It might make sense to process the data that the background thread has published, since the work is actually done and you might be interested in getting the GUI updated with as much info as you can (if that's what process() is doing, for example). And then it might not make sense if done() requires to have all the data processed and/or a call to process() after done() creates data/GUI inconsistencies.

如果你不希望在done()之后执行任何新进程(),那么有一个明显的解决方法,只需检查是否在<$ c中取消了worker $ c> process 方法也是如此!

There's an obvious workaround if you don't want any new process() to be executed after done(), simply check if the worker is cancelled in the process method too!

@Override
protected void process(List<String> chunks) {
    if (isCancelled()) return;
    String string = chunks.get(chunks.size() - 1);
    textArea.append(string);
}

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

It's more tricky to make done() be executed after that last process(), for example done could just use also a timer that will schedule the actual done() work after >DELAY. Although I can't think this is would be a common case since if you cancelled It shouldn't be important to miss one more process() when we know that we are in fact cancelling the execution of all the future ones.

这篇关于在process()调用完成之前执行SwingWorker,done()的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆