How it works
The explanation, with examples you can run.
Scopes and Jobs
Every coroutine has a Job in its context. Starting a coroutine inside a scope makes its Job a child of the scope's Job, so you get a tree. coroutineScope { } creates a scope inside a suspend function and suspends until every child is done:
import kotlinx.coroutines.* suspend fun price(id: Int): Int { delay(100L * id); return id * 10 } suspend fun total(ids: List<Int>): Int = coroutineScope { ids.map { id -> async { price(id) } } // all run concurrently .awaitAll() .sum()} fun main() = runBlocking { println(total(listOf(1, 2, 3))) // 60, after about 300 ms, not 600 val failing = runCatching { coroutineScope { launch { delay(1_000); println("never printed") } launch { delay(50); error("boom") } } } println(failing.exceptionOrNull()?.message) // boom: the slow sibling was cancelled}By default a child that fails with an exception cancels its parent, which cancels all the other children; then the exception surfaces from coroutineScope (or reaches the scope's CoroutineExceptionHandler, for a top-level launch). Where siblings should be independent, use supervisorScope { } or a scope built with SupervisorJob(): a failing child then fails alone. async keeps its exception until await() is called, but in a normal scope the failure still cancels the parent immediately.
Cancellation is cooperative
job.cancel() doesn't stop a thread. It marks the Job cancelled, and the next suspending call that checks (delay, yield, withContext, most kotlinx.coroutines functions) throws CancellationException. Code that never suspends has to check for itself:
import kotlinx.coroutines.* fun main() = runBlocking { val job = launch(Dispatchers.Default) { var n = 0L try { while (true) { ensureActive(); n++ } // without ensureActive this never stops } finally { withContext(NonCancellable) { println("cleanup after $n iterations") } } } delay(50) job.cancelAndJoin() println("cancelled: ${job.isCancelled}")}CancellationExceptionis how cancellation travels, so don't swallow it with a broadcatch (e: Exception); rethrow it.finallyblocks run on cancellation. Suspending inside them needswithContext(NonCancellable), because the coroutine is already cancelled.withTimeout(ms) { }cancels its block when time runs out and throwsTimeoutCancellationException;withTimeoutOrNullreturnsnullinstead.
Dispatchers
The dispatcher in a coroutine's context decides which thread runs it between suspensions:
Dispatchers.Default: a pool sized to the CPU cores, for computation.Dispatchers.IO: an elastic pool (64 threads by default, or more on large machines) for blocking calls. It shares threads with Default.Dispatchers.Main: the UI thread on Android, Swing or JavaFX; it needs the platform artifact.dispatcher.limitedParallelism(n): a view of a pool that runs at mostncoroutines at once, useful to protect a resource.
withContext(Dispatchers.IO) { ... } runs a block on another dispatcher and returns its result to the original one. Avoid GlobalScope and runBlocking in production code paths: the first escapes structured concurrency, the second blocks a thread to wait.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.