Structured Concurrency & Cancellation Flashcards
KOTLIN › Concurrency
- What are the core guarantees of structured concurrency in coroutines?
- Every coroutine runs in a scope that bounds its lifetime; a parent waits for all its children before completing; cancelling the parent cancels all children; and an uncaught failure in a child cancels the parent and its siblings (unless supervised). No coroutine is leaked or orphaned.
- coroutineScope vs supervisorScope - what is the difference?
- Both create a scope and wait for all children. In coroutineScope a failed child cancels the scope and all siblings (failures propagate up and down). In supervisorScope a child failure is isolated - siblings and the scope keep running. Use supervisorScope when children are independent.
- Job vs SupervisorJob?
- A regular Job propagates failure bidirectionally: a failing child cancels its parent and siblings. A SupervisorJob makes cancellation one-directional (only downward): a child's failure does not cancel the parent or siblings, but cancelling the parent still cancels children.
- Why is coroutine cancellation called 'cooperative', and how do you make CPU-bound code cancellable?
- Cancellation only sets a flag; a coroutine actually stops only when it suspends or checks for cancellation. A tight computation loop with no suspension points ignores cancel(). Make it cooperative by checking isActive, calling ensureActive(), or calling yield() inside the loop.
- isActive vs ensureActive() vs yield()?
- isActive is a boolean you poll (loop while it is true). ensureActive() throws CancellationException immediately if the job is cancelled (fail-fast, no boolean check). yield() also checks cancellation but additionally suspends to let other coroutines on the dispatcher run.
- Why must you never swallow CancellationException, and how do you handle it if caught?
- CancellationException is the signal that drives cooperative cancellation and propagates up the structured-concurrency tree. Catching it without rethrowing (e.g. catch(Exception)) breaks cancellation - the coroutine keeps running. Always rethrow it, and prefer catching specific types like IOException.
- When does a CoroutineExceptionHandler actually get invoked, and when is it ignored?
- It is invoked only for an uncaught exception from a launch coroutine that is a root of the scope (e.g. installed on the scope's context or in supervisorScope). It is ignored on async (the exception is stored in the Deferred and surfaces on await()), ignored when installed on a non-root child, and never catches CancellationException.
- How do exceptions differ between launch and async?
- launch treats an uncaught exception as unhandled immediately (propagates to parent / CoroutineExceptionHandler). async stores the exception in its Deferred and only throws it when you call await(); wrap await() in try/catch. As a root coroutine, async's exception is deferred until awaited.
- How do you fetch two resources in parallel and combine them safely?
- Use coroutineScope { val a = async { repoA.load() }; val b = async { repoB.load() }; Combined(a.await(), b.await()) }. Both run concurrently; coroutineScope waits for both and, if either fails, cancels the other and rethrows - so the suspend function fails atomically.