Why coroutines: threads are expensive
A program that calls a web API, a database and a file system spends most of its time waiting. The classic JVM answer is a thread per task: while one thread waits for the network, another runs. Threads work, but each one is an operating-system object with its own stack (around 1 MB reserved by default), and switching between them is done by the OS. A server with 10,000 open requests cannot comfortably hold 10,000 threads, and an Android app must never block its single UI thread at all.
Kotlin's standard library can start threads directly with thread { }. The program below is correct, but notice how much ceremony even four tasks need: create, start, remember, join. Printing only after every join() is what keeps the output deterministic.
import kotlin.concurrent.thread
fun main() {
val results = IntArray(4)
val workers = (0 until 4).map { i ->
thread(name = "worker-$i") {
results[i] = (1..1_000).sumOf { it * (i + 1) }
}
}
workers.forEach { it.join() } // wait for every thread before reading results
println(results.toList())
println("total = ${results.sum()}")
}[500500, 1001000, 1501500, 2002000]
total = 5005000Remove the join() line and run it a few times. Why can the printed list now contain zeros?
A coroutine is a computation that can pause in the middle (suspend) without holding a thread, and resume later — possibly on a different thread. While it is paused it is just a small object on the heap holding its local variables. Thousands of coroutines share a handful of threads. The documentation's classic demonstration starts 50,000 of them:
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(50_000) { // 50,000 concurrent coroutines
launch {
delay(5_000L) // suspends: the thread is free while this waits
print(".")
}
}
}Prints 50,000 dots after about five seconds, using one thread. The same program with thread { Thread.sleep(5000) } would create 50,000 OS threads and is likely to run out of memory.
Thread
- Scheduled by the operating system
- Blocks while waiting (
Thread.sleep, blocking I/O) - Around 1 MB of stack each; thousands is a lot
- Cancel by interrupting and hoping the code checks
Coroutine
- Scheduled by Kotlin onto a pool of threads
- Suspends while waiting (
delay, suspending I/O); the thread runs other work - A few hundred bytes each; hundreds of thousands is fine
- Cancellation and timeouts are built in and propagate to children
suspend keyword and the low-level machinery (Continuation, sequence { }) are in the language and standard library. Everything you use day to day — launch, async, runBlocking, delay, Dispatchers, Flow — lives in the kotlinx.coroutines library, added with one Gradle line: implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"). The runnable examples in this module use only the standard library; the library code is shown as snippets, with its output described exactly.