You launch a coroutine that runs a tight CPU-bound while-loop, then call cancel() on its Job. The loop keeps running. Why, and what is the simplest fix?
answer
- Cancellation is cooperative, not forced
- cancel() only sets state
- CPU loop has no suspension point
- while (isActive) is the simplest fix
- ensureActive/yield also work
basics
~10 sCancellation is cooperative: the coroutine only stops if it checks for cancellation. A pure CPU loop never checks, so it ignores cancel(). Add a check inside the loop, such as while (isActive).
solid answer
~40 sKotlin coroutine cancellation is cooperative. cancel() just sets the Job's state to cancelling; nothing forcibly stops the running code. Suspend functions in kotlinx.coroutines (delay, yield, etc.) cooperate by throwing CancellationException when resumed after cancel. A tight CPU loop calls no such suspend point, so it never observes cancellation and runs to completion. The simplest fix is to make the loop check the cancellation state: use `while (isActive)` (isActive is a CoroutineScope/CoroutineContext extension that is false once cancelled), or call `ensureActive()` / `yield()` inside the loop body. isActive is non-throwing and just stops the loop; ensureActive() throws CancellationException; yield() also throws and additionally lets other coroutines on the dispatcher run.
code
kotlin · 9 linesval job = launch(Dispatchers.Default) {
var i = 0L
while (isActive) { // checks cancellation each iteration
i++
}
println("stopped at $i")
}
delay(50)
job.cancelAndJoin() // now the loop actually stopsgo deeper
States cancellation is cooperative and adds while (isActive) to fix the loop.
Explains cancel() only sets state and lists isActive vs ensureActive vs yield with their throw/non-throw behavior.
Connects to how suspend functions act as check points and why CancellationException must not be swallowed.
Discusses structured-concurrency implications, dispatcher fairness via yield, and designing long compute loops with periodic checks and budgets.
## Why the loop ignores cancel() **Cooperative cancellation** means a coroutine is never killed from the outside. Calling `job.cancel()` only moves the `Job` into the *Cancelling* state and stores a `CancellationException`. The coroutine actually stops only when its code reaches a **cancellation check point** and observes that state. The built-in suspend functions in `kotlinx.coroutines` (`delay`, `yield`, `withContext`, channel/Flow operators, etc.) are the usual check points — when the coroutine is resumed after a cancel, they throw `CancellationException`. A tight loop like this contains no such point: ```kotlin val job = launch(Dispatchers.Default) { var i = 0L while (i < Long.MAX_VALUE) { i++ } // no suspension, no check } job.cancel() // sets state, but the loop never notices ``` So the loop runs to the end regardless of `cancel()`. ## How to make it cancellable Insert a cancellation check inside the loop. Three options: - **`isActive`** — a Boolean extension on `CoroutineScope`/`CoroutineContext`. It is `true` while active and `false` once cancelled. Non-throwing; you just exit the loop. ```kotlin while (isActive) { /* work */ } ``` - **`ensureActive()`** — checks the same state but **throws** `CancellationException` if cancelled, so the coroutine unwinds like any other suspend function would. - **`yield()`** — a `suspend` function that checks cancellation (throws if cancelled) **and** yields the thread so other coroutines can run. ## Key terms - **`Job`**: handle to a coroutine's lifecycle; `cancel()` requests cancellation. - **`CancellationException`**: the signal used to unwind a cancelled coroutine; it is treated specially and not logged as an error. - **CPU-bound**: work that keeps the thread busy computing (no I/O, no suspension), which is exactly the case that needs manual checks.
- Why is delay() cancellable but your while-loop is not?delay() is a suspend function that registers a cancellation handler; when the Job is cancelled it resumes with CancellationException. A plain loop suspends nowhere, so there is nothing to throw.
- Does isActive throw?No. isActive just returns false once cancelled; you decide how to exit. ensureActive() and yield() throw.
cancel() is like raising your hand to ask the runner to stop — they only stop if they occasionally look at you (the check inside the loop).
saying these in an interview costs you the question
- Claiming cancel() forcibly kills the coroutine / interrupts the thread
- Saying you must use Thread.stop() or interrupt()
- Not knowing cancellation is cooperative
- Thinking a CPU loop is automatically cancellable
- Catching and swallowing CancellationException as a fix