我正在玩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中的错误还是我的示例中的错误?
答案 0 :(得分:13)
显然,这是故意的。方法CompletableFuture::cancel的Javadoc声明:
[参数:] mayInterruptIfRunning - 此值在此实现中具有 no 效果,因为中断不用于控制处理。
有趣的是,方法ForkJoinTask::cancel对参数 mayInterruptIfRunning 使用几乎相同的措辞。
我猜这个问题:
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的上游。
这是幕后发生的事情。这不是精确的,而是一个方便的思维模型。
ForkJoinPool
内)。complete(...)
将供应商的结果传递给下一个CompletableFuture
下游。CompletableFuture
调用下一步 - 一个函数(步骤2),它接收上一步结果并返回一些将进一步传递给下游CompletableFuture
的{{ {1}}。complete(...)
会调用消费者CompletableFuture
。消费者完成后,下游System.out.println(s)
将收到它的值CompletableFuture
正如我们所看到的,此链中的每个(Void) null
都必须知道下游有谁在等待将值传递给他们的CompletableFuture
(或complete(...)
)。但是completeExceptionally(...)
不必知道它的上游(或上游 - 可能有几个)。
因此,在步骤3中调用CompletableFuture
不会中止步骤1和2 ,因为从步骤3到步骤2没有链接。
假设您使用cancel()
,那么您的步数就足够小,以便在执行一些额外步骤时没有任何损害。
如果要将取消传播到上游,您有两种选择:
CompletableFuture
(名称为CompletableFuture
),在每个步骤后检查(类似cancelled
)答案 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团队未实施中断是因为:
InputStream.read()
的调用被阻止,但Java I / O系统甚至都不可中断! (而且JDK团队没有计划再次使标准I / O系统可中断,就像在Java早期那样。)Object.finalize()
,Object.wait()
,Thread.stop()
等。我相信{{1 }}被认为是必须最终弃用并替换的事物类别。因此,较新的API(例如Thread.interrupt()
和ForkJoinPool
)已经不支持它。CompletableFuture
设计用于构建DAG结构的操作管道,类似于Java CompletableFuture
API。简洁地描述数据流DAG的一个节点的中断将如何影响其余DAG中的执行是非常困难的。 (当任何节点中断时,是否应立即取消所有并发任务?)解决这个问题的一种非常棘手的方法是让每个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
。