Как отменить Java 8 completable future?

Я играю с Java 8 completable futures. У меня есть следующий код:

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 или в моем примере?

4 ответов


видимо, это намеренно. Javadoc для метода класс CompletableFuture::отмена гласит:

[параметры] mayInterruptIfRunning - это значение нет эффект в этой реализации, потому что прерывания не используются для управления обработкой.

интересно, что метод ForkJoinTask::отмена использует почти такую же формулировку для параметра mayInterruptIfRunning.

У меня есть догадка по этому вопросу:

  • перерыв предназначен для использования с операциями блокировки, например сон, ждать или операции ввода-вывода,
  • , но ни CompletableFuture, ни ForkJoinTask предназначены для использования с блокированием операций.

вместо блокировки a CompletableFuture должны создайте новый CompletionStage, а задачи с привязкой к процессору являются необходимым условием для модели соединения вилки. Итак, используя перерыв С любым из них было бы поражение их цели. А с другой стороны, это может увеличить сложность, это не требуется, если используется по назначению.


когда вы называете CompletableFuture#cancel, вы останавливаете только нижестоящую часть цепи. Часть вверх по течению, i. e. что-то, что в конечном итоге вызовет complete(...) или completeExceptionally(...), не получает никакого сигнала о том, что результат больше не нужен.

что это 'вверх' и 'вниз' вещи?

давайте рассмотрим следующий код:

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

здесь, потоки данных сверху вниз-от создаваемого поставщиком, через быть доработанным функцией, К быть потребляется println. Часть выше определенного шага называется вверх по течению, а часть ниже-вниз по течению. Е. Г. шаги 1 и 2 вверх на Шаге 3.

вот что происходит за кулисами. Это не точная, а скорее удобная мысленная модель того, что происходит.

  1. поставщик (Шаг 1)выполняется (внутри общегоForkJoinPool).
  2. результат поставщика после этого передается complete(...) до следующего CompletableFuture нисходящий.
  3. получив результат, это CompletableFuture вызывает следующий шаг-функция (Шаг 2), которая принимает результат предыдущего шага и возвращает что-то, что будет передано дальше, вниз по течению CompletableFuture ' s complete(...).
  4. получив результат Шаг 2, Шаг 3 CompletableFuture вызывает у потребителя, System.out.println(s). После того, как потребитель закончит, нисходящий CompletableFuture получит его значение,(Void) null

как видим, каждый CompletableFuture в этой цепочке должен знать, кто там вниз по течению, ожидая, когда значение будет передано их complete(...) (или completeExceptionally(...)). Но ... --7--> Не нужно ничего знать об этом вверх по течению (или вверх по течению - может быть несколько).

таким образом, называя cancel() на Шаге 3 не прерывает шаги 1 и 2, потому что нет ссылки с шага 3 на Шаг 2.

предполагается, что если вы используете CompletableFuture тогда ваши шаги достаточно малы, так что нет никакого вреда, если пару лишних шагов казнить.

если вы хотите, чтобы отмена будет распространяться вверх по течению, у вас есть два варианта:

  • реализовать это самостоятельно - создать выделенную CompletableFuture (назовите его как cancelled), которое проверяется после каждого шага (что-то вроде step.applyToEither(cancelled, Function.identity()))
  • используйте реактивный стек, такой как RxJava 2, ProjectReactor/Flux или Akka Streams

вам нужна альтернативная реализация CompletionStage для выполнения истинного прерывания потока. Я только что выпустил небольшую библиотеку, которая служит именно этой цели -https://github.com/vsilaev/tascalate-concurrent


исключение CancellationException является частью внутренней процедуры отмены ForkJoin. Исключение появится, когда вы получите результат future:

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

потребовалось некоторое время, чтобы увидеть это в отладчике. JavaDoc не так ясно, что происходит или чего вы должны ожидать.