Питання 18 · Розділ 19

Як скасувати виконання CompletableFuture

CompletableFuture можна скасувати методом cancel(), але є нюанси:

Мовні версії: English Russian Ukrainian

🟢 Junior Level

CompletableFuture можна скасувати методом cancel(), але є нюанси:

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    Thread.sleep(5000);
    return "Done";
});

// Відміна
boolean cancelled = cf.cancel(true);  // true = mayInterruptIfRunning

System.out.println(cf.isCancelled());  // true

Важливо: cancel() не завжди зупиняє виконання! Він тільки встановлює статус cancelled.


🟡 Middle Level

Як працює cancel

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    // Ця задача продовжить виконуватися навіть після cancel!
    Thread.sleep(5000);
    return "Done";
});

cf.cancel(false);  // не перериває

// Задача все ще виконується у фоні!

Чому: cancel() встановлює результат у CancellationException, але не перериває потік.

Правильна відміна

1. Cooperative cancellation:

AtomicBoolean cancelled = new AtomicBoolean(false);

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    for (int i = 0; i < 1000; i++) {
        if (cancelled.get()) {
            throw new CancellationException("Task cancelled");
        }
        // do work
    }
    return "Done";
});

// Відміна
cancelled.set(true);
cf.cancel(false);

2. Timeout:

cf.orTimeout(5, TimeUnit.SECONDS);  // автовідміна через 5 секунд

Типові помилки

  1. Очікування переривання: ```java // ❌ cancel() не перериває потік cf.cancel(true); // задача продовжує виконуватися

// ✅ Cooperative cancellation if (Thread.currentThread().isInterrupted()) { throw new CancellationException(); }


---

## 🔴 Senior Level

### Internal Implementation

```java
// На відміну від FutureTask, CompletableFuture.cancel(true) намагається
// перервати потік через Thread.interrupt(). Але це працює тільки
// якщо задача перевіряє Thread.interrupted() або виконує блокуючу
// операцію, що реагує на interrupt.
// На практиці cooperative cancellation (через volatile flag) — основний підхід.

Архітектурні Trade-offs

Підхід Плюси Мінуси
cancel() Просто Не перериває
Cooperative Надійно Потрібно писати код
Timeout Авто Тільки час

Edge Cases

1. Dependent CFs:

CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> cf2 = cf1.thenApply(s -> s + " World");

cf1.cancel(false);
// cf2 теж буде скасований — залежить від cf1

2. Interrupt flag:

// Результат залежить від таймінгу. Якщо задача вже завершилася — cancel()
// поверне false і isCancelled() буде false. Якщо задача ще виконується
// і реагує на interrupt — буде true. Thread.sleep() реагує на interrupt.

// Thread.interrupted() може допомогти
CompletableFuture.supplyAsync(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // work
    }
    throw new CancellationException();
});

Production Experience

public class CancellableTask {
    private volatile boolean cancelled = false;
    private CompletableFuture<String> future;

    public CompletableFuture<String> start() {
        future = CompletableFuture.supplyAsync(() -> {
            while (!cancelled) {
                // work
            }
            if (cancelled) {
                throw new CancellationException();
            }
            return "Done";
        });
        return future;
    }

    public void cancel() {
        cancelled = true;
        if (future != null) {
            future.cancel(false);
        }
    }
}

Best Practices

// ✅ Cooperative cancellation
AtomicBoolean cancelled = new AtomicBoolean(false);

// ✅ Timeout
cf.orTimeout(5, TimeUnit.SECONDS);

// ✅ Перевірка interrupt flag
if (Thread.currentThread().isInterrupted()) throw new CancellationException();

// ❌ Очікування що cancel() перерве потік
// ❌ Ігнорування CancellationException

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • cancel(true) намагається перервати потік через Thread.interrupt(), але на практиці не працює
  • Cooperative cancellation (volatile flag / AtomicBoolean) — основний підхід
  • orTimeout() — автоматична відміна за часом
  • При відміні кореневого CF всі залежні завершаться з CancellationException
  • Відміна дочірньої цепочки НЕ впливає на кореневий CF

Часті уточнюючі питання:

  • cancel(true) перерве виконання? — Ні, тільки встановлює статус. Задача продовжує працювати
  • Як правильно скасувати? — Cooperative: AtomicBoolean flag + перевірка в циклі + cancel(false)
  • Що з залежними CF при відміні? — Завершуються з CancellationException
  • obtrudeValue для відміни? — Ні, obtrudeValue примусово змінює результат — dangerous, race conditions

Червоні прапорці (НЕ говорити):

  • «cancel(true) гарантовано зупиняє задачу» — тільки interrupt flag, задача може ігнорувати
  • «Після cancel() задача одразу завершується» — продовжує виконуватися, потрібна cooperative cancellation
  • «obtrudeValue — нормальний спосіб відміни» — порушує контракт одноразового завершення, race conditions

Пов’язані теми:

  • [[20. Як реалізувати timeout для CompletableFuture]]
  • [[26. Чи можна вручну завершити CompletableFuture результатом]]
  • [[19. Чи можна повторно використовувати один CompletableFuture в декількох ланцюжках]]
  • [[18. Що станеться, якщо в ланцюжку CompletableFuture виникне виключення]]