Knowing the machinery explains stack traces, performance, thread switching and why blocking calls hurt.
The compiler transforms every suspend fun using continuation-passing style (CPS). A function suspend fun load(id: Int): User becomes, on the JVM, roughly Object load(int id, Continuation<? super User> cont).
The extra Continuation parameter represents "the rest of the computation". Its resumeWith(Result<T>) method is called when the awaited result is available.
The function body is compiled into a state machine: each suspension point becomes a label, and local variables that live across suspension points are saved into fields of a continuation object. When the function calls another suspend function:
- If the callee completes immediately, it returns the value directly and execution continues without suspending.
- If it needs to wait, it returns the special marker
COROUTINE_SUSPENDED. The whole call chain returns, freeing the thread. Later,resumeWithre-enters the state machine at the saved label.
The low-level primitive is suspendCoroutine / suspendCancellableCoroutine { cont -> ... }, which bridges callback APIs: register a callback that calls cont.resume(value). kotlinx.coroutines' delay, channels and await are built on it.
Dispatchers are ContinuationInterceptors: they wrap the continuation so resume is posted to a thread pool or UI thread rather than run inline. That's how withContext moves between threads.
Consequences: a suspended coroutine costs only its small continuation objects, not a thread stack; stack traces show the state machine (use the coroutines debug agent for readable traces); and blocking a thread inside a coroutine blocks every coroutine scheduled on it.
Example
import kotlin.coroutines.*
// A minimal coroutine runtime, no kotlinx.coroutines needed
var saved: Continuation<String>? = null
suspend fun waitForValue(): String = suspendCoroutine { cont ->
println("suspending")
saved = cont // keep "the rest of the computation"
}
suspend fun program(): Int {
val a = waitForValue() // state 0 -> 1
println("resumed with $a")
val b = waitForValue() // state 1 -> 2
return (a + b).length
}
fun main() {
val done = Continuation<Int>(EmptyCoroutineContext) { println("finished: ${it.getOrNull()}") }
::program.startCoroutine(done) // suspending
println("main thread is free") // main thread is free
saved!!.resume("hello") // resumed with hello / suspending
saved!!.resume("!!") // finished: 7
}Remember
- suspend functions get a hidden Continuation parameter (CPS)
- Bodies compile to state machines; locals live in continuation fields
- COROUTINE_SUSPENDED unwinds the call stack and frees the thread
- Dispatchers intercept continuations to resume on chosen threads
Watch out for
- Assuming a suspend function always suspends; it may complete synchronously.