【问题标题】:Can't stop a Runnable once I've passed it to Swing's invokeAndWait within a different thread一旦我将 Runnable 传递给不同线程中的 Swing 的 invokeAndWait,就无法停止它
【发布时间】:2014-04-30 21:08:27
【问题描述】:

我有一个带有固定线程池的类,我用它来多次运行一个过程。该过程的一部分涉及创建一个 Runnable 并传递给 SwingUtilities.invokeAndWait 以更新我的 Swing GUI。

当我发出再次运行程序的请求时,我希望能够抢占任何正在运行的程序。为此,我保留了上次submit() 调用中的Future,以便我可以cancel() 它。我还保留了最后一个Runnable,所以我可以在它上面设置一个标志,告诉它在run() 时什么都不做,因为它可能已传递给 AWT 事件线程,因此我无法通过取消我的@来停止它987654328@.

但是,无论我做什么,Runnable 都会被执行。这是一个精简的示例:

class Procedure {
    private AtomicReference<Updater> updater;
    private AtomicReference<Future<?>> currProcedure;
    private ExecutorService threadPool;
    private Runnable theProcedure;

    public Procedure() {
        updater = new AtomicReference<Updater>();
        currProcedure = new AtomicReference<Future<?>>();
        threadPool = Executors.newFixedThreadPool(1);
        theProcedure = new Runnable() {
            public void run() {
                doProcedure();
            }
        };
    }
    private void doProcedure() {
        // a bunch of stuff
        updater.set(new Updater());
        try {
            SwingUtilities.invokeAndWait(updater.get());
        } catch (InvocationTargetException | InterruptedException ex) {

        }
        // some more stuff
    }
    public void execute() {
        try {
            synchronized(this) {
                if (null != currProcedure.get()) {
                    currProcedure.get().cancel(true);
                }
                if (null != updater.get()) {
                    updater.get().cancel();
                }
            }
            currProcedure.set(threadPool.submit(theProcedure));
        } catch (RejectedExecutionException ex) {

        }
    }
}

class Updater implements Runnable {
    private AtomicBoolean cancelled;

    public Updater() {
        cancelled = new AtomicBoolean(false);
    }
    public void cancel() {
        cancelled.set(true);
    }
    public void run() {
        if (cancelled.get()) {
            return;
        }
        // do the GUI update
    }
}

它会像这样使用:

Procedure p = new Procedure();
p.execute();
p.execute();

实际上,我是从 AWT 事件线程调用Procedure.execute(),所以我认为这可能与它有关;除此之外,我不知道我做错了什么或如何实现我的目标。有什么帮助吗?

编辑:我也尝试将cancel() 我的Updater 退出AWT 事件线程,但无论如何都没有运气:

if (null != currProcedure.get()) {
    currProcedure.get().cancel(true);
}
if (null != updater.get()) {
    threadPool.submit(new Runnable() {
        public void run() {
            updater.get().cancel();
        }
    });
}

编辑 2:我的日志语句似乎暗示我成功中断了updater(我得到了一个java.lang.InterruptedException)但run() 仍然执行(并且cancelled.get() 仍然是@ 987654340@ 在它的末尾)。为什么cancel(true) 不会停止updater 的执行?

【问题讨论】:

  • 当您传递到事件队列时,Runnable 将被执行,但我假设您的意思是它没有看到取消状态?
  • @MadProgrammer 正是如此。我很难简洁地表达出来。

标签: java multithreading swing future executorservice


【解决方案1】:

您的代码以多种方式损坏。您似乎期望AtomicReference 神奇地修复您的竞争条件,但顾名思义,它提供的只是对引用的原子 访问。如果您多次访问该对象,则您有多个访问,每个访问都是原子的,但在一起仍然不是线程安全的。

第一个例子:

updater.set(new Updater());
try {
    SwingUtilities.invokeAndWait(updater.get());
} catch (InvocationTargetException | InterruptedException ex) {
}

set 和以下get 调用都是原子调用,但谁说您将在get 上收到的引用仍然是您之前拥有的set 引用?例如,您可能会收到一个更新的 Updater 实例,该实例由另一个线程调度,从而导致一个 Updater 永远不会发送到 EDT,而另一个会发送两次。

第二个例子:

if (null != currProcedure.get()) {
    currProcedure.get().cancel(true);
}
if (null != updater.get()) {
    updater.get().cancel();
}

两次同样的错误。您正在检查AtomicReference.get()null 的结果,但在该调用中不是null,谁说在 next 调用中它仍然不是null?您在 synchronized 块中有该代码,但由于其他线程可以访问相同的变量,例如从doProcedure() 内部没有同步,它不提供任何保护。也许您从未重置对null 的引用(这将是另一个错误)所以这里没有问题,但它清楚地表明了对如何使用AtomicReferences 的误解。


此外,你说你是这样使用它的:

Procedure p = new Procedure();
p.execute();
p.execute();

所以你一个接一个地调用execute,这些执行方法试图取消他们在updater中找到的任何东西立即但是此时后台可能正在执行你的“一堆东西” ”并且没有设法设置其updater(如Harald has pointed out)。所以execute 调用可能会看到null 或一个非常过时的Updater 实例,之后后台线程设置第一个Updater,然后设置第二个Updater,它们都不是曾经取消过。注意后台线程的中断会结束invokeAndWait等待部分,但不会取消提供的runnable的执行


既然您正在使用AtomicReference,您应该开始真正使用它。原子更新是实现预期逻辑的关键特性,例如:

Updater newUpdater=new Updater();
Updater old=updater.getAndSet(newUpdater);
if(old!=null) old.cancel();
SwingUtilities.invokeLater(newUpdater);

通过读取旧更新程序并自动设置新更新程序,您可以确保每个新的Updater 都与可能取消的旧更新程序配对。并且通过将值保存在局部变量中而不是多次读取它们,您可以确保不会有中间更新更改引用。这甚至适用于多个后台线程,并且不需要额外的 synchronized 块。

调度Updater 的调用已更改为invokeLater,因为在单线程执行程序等待Updater 完成的情况下,这意味着线程永远不会设置新的@987654356 @ 并取消旧的。

请注意,Updater 本身应在完成时将updater 引用设置为null。取消已经完成的Updater 是没有意义的,但是由于updater 引用是一个共享引用,它的存在时间可能远长于Updater 的(预期)生命周期,因此应该清除它以便可以垃圾会立即收集,而不是在设置下一个 Updater 之前到处乱扔。

【讨论】:

  • 感谢您的所有解释。我肯定误解了如何使用AtomicReference
【解决方案2】:

在我看来,问题出在一线

// a bunch of stuff

在执行一堆东西时,我们有以下情况

  1. currProcedure 包含 theProcedure 的第 N 次运行,而
  2. updater 仅包含 Updater 的第 (N-1) 个实例。

因此,您在 synchronized(true) 块中的两个 cancel 调用不会针对匹配的项目对。事实上,(N-1)st Updater 被取消了,但那个已经完成了。另一方面,“一堆东西”可能并不关心在Future 上调用的cancel(true),因此一旦完成,“一堆东西”之后的行就会不受阻碍地运行并安排下一个Updater .

要解决这种情况,以下可能会有所帮助。在“一堆东西”之后,放上以下代码:

synchronized(this) {
  if (!Thread.currentThread().isInterrupted()) {
    // copy the lines after "a bunch of stuff here to schedule the updater
  }
}

execute() 内,您无需担心取消Updater。它不需要任何取消功能。

您现在已将两个 synchronized 块正确排序到事件中。一个新的execute() 首先进入同步块。然后它可以抢占正在运行的doProcedure 调度Updater,或者doProcedure 使其首先进入同步状态。那么,Updater 值得运行,不是吗?必须有一个不归路。

我在这里假设Future 上的cancel(true) 最终成为底层线程上的.interrupt()(目前不确定,请查一下)。如果没有,你肯定会找到一种方法来以某种方式中断doProcedure 线程。

【讨论】:

  • 您对cancel(true) 所做的事情是正确的。谢谢,我合并了这样的支票。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-30
相关资源
最近更新 更多