Как отменить будущее Java 8?

Я играю с 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, я планирую выполнение кода, который ждет на защелке. Затем я отменяю будущее, ожидая, что прерванное исключение будет выбрано внутри. Но кажется, что поток остается заблокированным в ожидании вызова, и InterruptedException никогда не выбрасывается, даже если будущее отменяется (утверждения проходят). Эквивалентный код с использованием ExecutorService работает должным образом. Является ли это ошибкой в ​​CompletableFuture или в моем примере?

Ответ 1

По-видимому, это намеренно. Javadoc для метода CompletableFuture:: cancel утверждает:

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

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

У меня есть предположение по этой проблеме:

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

Вместо блокировки, CompletableFuture должен создать новый CompletionStage, а задачи cpu-bound являются обязательным условием для модели fork-join. Таким образом, использование прерывания с любым из них может победить их цель. А с другой стороны, это может увеличить сложность, которая не требуется, если используется по назначению.

Ответ 2

Когда вы вызываете CompletableFuture#cancel, вы останавливаете только нижнюю часть цепочки. Верхняя часть, т.е. е. то, что в конечном итоге вызовет 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) (внутри JVM common ForkJoinPool).
  • Результат поставщика затем передается complete(...) в следующий CompletableFuture ниже по течению.
  • После получения результата CompletableFuture вызывает следующий шаг - функцию (шаг 2), которая принимает результат предыдущего шага и возвращает то, что будет передано дальше, в нисходящий поток CompletableFuture complete(...).
  • После получения результата шага 2 шаг 3 CompletableFuture вызывает пользователя System.out.println(s). После того, как потребитель будет готов, нижестоящий CompletableFuture получит это значение, (Void) null

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

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

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

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

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

Ответ 3

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

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

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

Ответ 4

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