如何取消Java 8可完成的未来?

时间:2014-04-27 07:03:29

标签: java multithreading java-8 completable-future

我正在玩Java 8可完成的期货。我有以下代码:

CountDownLatch waitLatch = new CountDownLatch(1);

CompletableFuture<?> future = CompletableFuture.runAsync(() -> {
    try {
        System.out.println("Wait");
        waitLatch.await(); //cancel should interrupt
        System.out.println("Done");
    } catch (InterruptedException e) {
        System.out.println("Interrupted");
        throw new RuntimeException(e);
    }
});

sleep(10); //give it some time to start (ugly, but works)
future.cancel(true);
System.out.println("Cancel called");

assertTrue(future.isCancelled());

assertTrue(future.isDone());
sleep(100); //give it some time to finish

使用runAsync我计划执行等待锁存器的代码。接下来我取消了未来,期望被抛入中断的异常。但似乎线程在await调用上仍然被阻塞,即使未来被取消(断言传递),也不会抛出InterruptedException。使用ExecutorService的等效代码按预期工作。它是CompletableFuture中的错误还是我的示例中的错误?

6 个答案:

答案 0 :(得分:13)

显然,这是故意的。方法CompletableFuture::cancel的Javadoc声明:

  

[参数:] mayInterruptIfRunning - 此值在此实现中具有 no 效果,因为中断不用于控制处理。

有趣的是,方法ForkJoinTask::cancel对参数 mayInterruptIfRunning 使用几乎相同的措辞。

我猜这个问题:

  • 中断旨在与阻止操作一起使用,例如 sleep wait 或I / O操作,
  • CompletableFuture ForkJoinTask 都不适用于阻止操作。

CompletableFuture 应该创建一个新的 CompletionStage ,而不是阻塞,而cpu绑定任务是fork-join模型的先决条件。因此,使用中断与其中任何一个都会失败。而另一方面,它可能会增加复杂性,如果按预期使用则不需要。

答案 1 :(得分:6)

当您致电CompletableFuture#cancel时,您只会停止链的下游部分。上游部分,i。即最终会调用complete(...)completeExceptionally(...)的内容,不会得到任何不再需要结果的信号。

那些“上游”和“下游”的东西是什么?

让我们考虑以下代码:

CompletableFuture
        .supplyAsync(() -> "hello")               //1
        .thenApply(s -> s + " world!")            //2
        .thenAccept(s -> System.out.println(s));  //3

在这里,数据从上到下流动 - 从供应商创建,到功能修改,再到println消费。上述特定步骤的部分称为上游部分,下部部分称为下游部分。 E. g。步骤1和2是步骤3的上游。

这是幕后发生的事情。这不是精确的,而是一个方便的思维模型。

  1. 正在执行供应商(步骤1)(在JVM的共同ForkJoinPool内)。
  2. 然后,complete(...)将供应商的结果传递给下一个CompletableFuture下游。
  3. 收到结果后,CompletableFuture调用下一步 - 一个函数(步骤2),它接收上一步结果并返回一些将进一步传递给下游CompletableFuture的{​​{ {1}}。
  4. 收到第2步结果后,第3步complete(...)会调用消费者CompletableFuture。消费者完成后,下游System.out.println(s)将收到它的值CompletableFuture
  5. 正如我们所看到的,此链中的每个(Void) null都必须知道下游有谁在等待将值传递给他们的CompletableFuture(或complete(...))。但是completeExceptionally(...)不必知道它的上游(或上游 - 可能有几个)。

    因此,在步骤3中调用CompletableFuture不会中止步骤1和2 ,因为从步骤3到步骤2没有链接。

    假设您使用cancel(),那么您的步数就足够小,以便在执行一些额外步骤时没有任何损害。

    如果要将取消传播到上游,您有两种选择:

    • 自己实施 - 创建一个专用的CompletableFuture(名称为CompletableFuture),在每个步骤后检查(类似cancelled
    • 使用反应堆栈,如RxJava 2,ProjectReactor / Flux或Akka Streams

答案 2 :(得分:2)

您需要CompletionStage的替代实现来完成真正的线程中断。我刚刚发布了一个完全符合此目的的小型图书馆 - https://github.com/vsilaev/tascalate-concurrent

答案 3 :(得分:0)

CancellationException是内部ForkJoin取消例程的一部分。检索未来结果时会出现异常:

try { future.get(); }
      catch (Exception e){
          System.out.println(e.toString());            
      }

花了一段时间在调试器中看到这个。 JavaDoc并不清楚发生了什么或者你应该期待什么。

答案 4 :(得分:0)

如果您实际上希望能够取消任务,则必须使用Future本身(例如,由ExecutorService.submit(Callable<T>)返回,而不是CompletableFuture。)通过 nosid CompletableFuture会完全忽略对cancel(true)的任何呼叫。

我怀疑JDK团队未实施中断是因为:

  1. 中断总是很困难,人们很难理解,也很难使用。尽管对InputStream.read()的调用被阻止,但Java I / O系统甚至都不可中断! (而且JDK团队没有计划再次使标准I / O系统可中断,就像在Java早期那样。)
  2. JDK团队一直在努力淘汰Java早期的旧API,例如Object.finalize()Object.wait()Thread.stop()等。我相信{{1 }}被认为是必须最终弃用并替换的事物类别。因此,较新的API(例如Thread.interrupt()ForkJoinPool)已经不支持它。
  3. CompletableFuture设计用于构建DAG结构的操作管道,类似于Java CompletableFuture API。简洁地描述数据流DAG的一个节点的中断将如何影响其余DAG中的执行是非常困难的。 (当任何节点中断时,是否应立即取消所有并发任务?)
  4. 鉴于JDK和库如今已达到内部复杂性水平,我怀疑JDK团队只是不想处理正确的中断。 (lambda系统的内部-嗯。)

解决这个问题的一种非常棘手的方法是让每个Stream都将对自身的引用导出到外部可见的CompletableFuture,然后可以在需要时直接中断AtomicReference引用。另一个外部线程。或者,如果您使用自己的Thread在自己的ExecutorService中启动所有任务,则即使ThreadPool拒绝通过以下方式触发中断,您也可以手动中断已启动的任何或所有线程CompletableFuture。 (请注意,尽管cancel(true) lambda不能引发已检查的异常,所以如果您在CompletableFuture中有可中断的等待,则必须将其作为未检查的异常重新抛出。)

更简单地说,您可以在外部作用域中声明CompletableFuture,并从每个AtomicReference<Boolean> cancel = new AtomicReference<>()任务的lambda内部定期检查此标志。

您还可以尝试设置CompletableFuture实例的DAG而不是Future实例的DAG,那样您就可以准确地指定任何一项任务中的异常和中断/取消对其他任务的影响当前正在运行的任务。 I show how to do this in my example code in my question here,并且效果很好,但是很多样板。

答案 5 :(得分:0)

即使调用了 Future.cancel(..),对等待的调用仍然会阻塞。正如其他人提到的,CompletableFuture 不会使用中断来取消任务。

根据CompletableFuture.cancel(..)的javadoc:

<块引用>

mayInterruptIfRunning 这个值在这个实现中没有影响,因为中断不用于控制处理。

即使实现会导致中断,您仍然需要阻塞操作才能取消任务或通过 Thread.interrupted() 检查状态。

与中断 Thread 不同(这可能并不总是容易做到),您可以在操作中设置检查点,您可以在其中优雅地终止当前任务。这可以在将要处理的某些元素的循环中完成,或者您在操作的每个步骤之前检查取消状态并自己抛出 CancellationException

棘手的部分是在任务中获取 CompletableFuture 的引用以调用 Future.isCancelled()。以下是如何完成的示例:

public abstract class CancelableTask<T> {

    private CompletableFuture<T> task;

    private T run() {
        try {
            return compute();
        } catch (Throwable e) {
            task.completeExceptionally(e);
        }
        return null;
    }

    protected abstract T compute() throws Exception;

    protected boolean isCancelled() {
        Future<T> future = task;
        return future != null && future.isCancelled();
    }

    public Future<T> start() {
        synchronized (this) {
            if (task != null) throw new IllegalStateException("Task already started.");
            task = new CompletableFuture<>();
        }
        return task.completeAsync(this::run);
    }
}

编辑:这里改进的 CancelableTask 版本作为静态工厂:

public static <T> CompletableFuture<T> supplyAsync(Function<Future<T>, T> operation) {
    CompletableFuture<T> future = new CompletableFuture<>();
    return future.completeAsync(() -> operation.apply(future));
}

这里是测试方法:

@Test
void testFuture() throws InterruptedException {
    CountDownLatch started = new CountDownLatch(1);
    CountDownLatch done = new CountDownLatch(1);

    AtomicInteger counter = new AtomicInteger();
    Future<Object> future = supplyAsync(task -> {
        started.countDown();
        while (!task.isCancelled()) {
            System.out.println("Count: " + counter.getAndIncrement());
        }
        System.out.println("Task cancelled");
        done.countDown();
        return null;
    });

    // wait until the task is started
    assertTrue(started.await(5, TimeUnit.SECONDS));
    future.cancel(true);
    System.out.println("Cancel called");

    assertTrue(future.isCancelled());

    assertTrue(future.isDone());
    assertTrue(done.await(5, TimeUnit.SECONDS));
}

如果您真的想在 CompletableFuture 之外使用中断,那么您可以将自定义 Executor 传递给 CompletableFuture.completeAsync(..),您可以在其中创建自己的 Thread,覆盖 { {1}} 在 cancel(..) 中并打断您的 CompletableFuture