问题
I am playing with Java 8 completable futures. I have the following code:
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
Using runAsync I schedule execution of a code that waits on a latch. Next I cancel the future, expecting an interrupted exception to be thrown inside. But it seems that the thread remains blocked on the await call and the InterruptedException is never thrown even though the future is canceled (assertions pass). An equivalent code using ExecutorService works as expected. Is it a bug in the CompletableFuture or in my example?
回答1:
Apparently, it's intentional. The Javadoc for the method CompletableFuture::cancel states:
[Parameters:] mayInterruptIfRunning - this value has no effect in this implementation because interrupts are not used to control processing.
Interestingly, the method ForkJoinTask::cancel uses almost the same wording for the parameter mayInterruptIfRunning.
I have a guess on this issue:
- interruption is intended to be used with blocking operations, like sleep, wait or I/O operations,
- but neither CompletableFuture nor ForkJoinTask are intended to be used with blocking operations.
Instead of blocking, a CompletableFuture should create a new CompletionStage, and cpu-bound tasks are a prerequisite for the fork-join model. So, using interruption with either of them would defeat their purpose. And on the other hand, it might increase complexity, that's not required if used as intended.
回答2:
When you call CompletableFuture#cancel
, you only stop the downstream part of the chain. Upstream part, i. e. something that will eventually call complete(...)
or completeExceptionally(...)
, doesn't get any signal that the result is no more needed.
What are those 'upstream' and 'downstream' things?
Let's consider the following code:
CompletableFuture
.supplyAsync(() -> "hello") //1
.thenApply(s -> s + " world!") //2
.thenAccept(s -> System.out.println(s)); //3
Here, the data flows from top to bottom - from being created by supplier, through being modified by function, to being consumed by println
. The part above particular step is called upstream, and the part below is downstream. E. g. steps 1 and 2 are upstream for step 3.
Here's what happens behind the scenes. This is not precise, rather it's a convenient mind model of what's going on.
- Supplier (step 1) is being executed (inside the JVM's common
ForkJoinPool
). - The result of the supplier is then being passed by
complete(...)
to the nextCompletableFuture
downstream. - Upon receiving the result, that
CompletableFuture
invokes next step - a function (step 2) which takes in previous step result and returns something that will be passed further, to the downstreamCompletableFuture
'scomplete(...)
. - Upon receiving the step 2 result, step 3
CompletableFuture
invokes the consumer,System.out.println(s)
. After consumer is finished, the downstreamCompletableFuture
will receive it's value,(Void) null
As we can see, each CompletableFuture
in this chain has to know who are there downstream waiting for the value to be passed to their's complete(...)
(or completeExceptionally(...)
). But the CompletableFuture
don't have to know anything about it's upstream (or upstreams - there might be several).
Thus, calling cancel()
upon step 3 doesn't abort steps 1 and 2, because there's no link from step 3 to step 2.
It is supposed that if you're using CompletableFuture
then your steps are small enough so that there's no harm if a couple of extra steps will get executed.
If you want cancellation to be propagated upstream, you have two options:
- Implement this yourself - create a dedicated
CompletableFuture
(name it likecancelled
) which is checked after every step (something likestep.applyToEither(cancelled, Function.identity())
) - Use reactive stack like RxJava 2, ProjectReactor/Flux or Akka Streams
回答3:
You need an alternative implementation of CompletionStage to accomplish true thread interruption. I've just released a small library that serves exactly this purpose - https://github.com/vsilaev/tascalate-concurrent
回答4:
The CancellationException is part of the internal ForkJoin cancel routine. The exception will come out when you retrieve the result of future:
try { future.get(); }
catch (Exception e){
System.out.println(e.toString());
}
Took a while to see this in a debugger. The JavaDoc is not that clear on what is happening or what you should expect.
来源:https://stackoverflow.com/questions/23320407/how-to-cancel-java-8-completable-future