Part 3 · 1 chapters · ~8 min

Coroutines

Suspend functions and the compiler's state machines, launch and async, structured concurrency with scopes and cancellation, dispatchers (Default, IO, Main), exception handling and SupervisorJob, timeouts, Flow for streams and backpressure, channels, measured coroutine costs, and coroutines versus Java virtual threads.

4

Measured, then structured

code
val ms = measureTimeMillis {
    coroutineScope { repeat(100_000) { launch(Dispatchers.Default) { delay(1000); done.incrementAndGet() } } }
}
// run 1: 1,740 ms · run 2: 1,438 ms · all 100,000 completed

// structured concurrency: a failure cancels siblings and propagates
coroutineScope {
    launch { delay(100); throw IllegalStateException("rail down") }
    launch { delay(10_000); println("never printed") }        // cancelled
}

// fan out with a deadline, blocking calls on the IO dispatcher
suspend fun balances(ids: List<String>): List<Balance> = withTimeout(2_000) {
    ids.map { id -> async(Dispatchers.IO) { ledgerClient.balance(id) } }.awaitAll()
}

// Flow: a cold stream with backpressure
fun postings(accountId: String): Flow<Posting> = flow { while (true) { emitAll(repo.since(accountId, cursor)); delay(500) } }
COROUTINES, MEASURED
Kotlin 2.4.20, kotlinx.coroutines 1.10.2, JVM 26, Apple M3 Pro (12 cores)
100,000 coroutines × delay(1 s), run 11,740 ms totalsame, run 2 (warm JIT)1,438 ms totalideal (all wait concurrently)1,000 ms
swipe the figure sideways, or tap expand for full screen
1/4
cheap waiting
Launching 100,000 coroutines that each suspend for one second finished in 1.4 to 1.7 seconds total: they waited concurrently, not one after another (which would take 28 hours).
100k concurrent waits~1.4 s, not 28 h