How it works
The explanation, with examples you can run.
The hidden Continuation
suspend fun loadProfile(id: String): Profile { val user = api.user(id) // suspension point 1 val avatar = api.avatar(user) // suspension point 2 return Profile(user, avatar)}compiles to something shaped roughly like this (simplified):
fun loadProfile(id: String, cont: Continuation<Profile>): Any? { val sm = cont as? LoadProfileSM ?: LoadProfileSM(cont) // one state object per call when (sm.label) { 0 -> { sm.label = 1 val r = api.user(id, sm); if (r === COROUTINE_SUSPENDED) return r sm.user = r as User } 1 -> { sm.user = sm.result as User } } // ...the same again for label 2 and avatar... return Profile(sm.user, sm.avatar)}- An extra
Continuationparameter is added: an object with aresumeWith(result)method, which is how the callee gets back to its caller. This is continuation-passing style. - The body becomes a state machine. Each suspension point gets a label; locals that are needed after it are saved as fields of the state object.
- If a callee really pauses (say, waiting for the network), it returns the special value
COROUTINE_SUSPENDED, and every caller up the chain returns it too. The thread is free. - When the result arrives, someone calls
resumeWith, which callsloadProfileagain with the same state object; thewhenjumps to the saved label.
If a callee has the result ready (a cache hit), it just returns it and nothing is suspended, which is why suspend calls are cheap when they don't wait.
Building your own suspension point
At the bottom of every chain, something turns a callback into a suspension. You can do it yourself:
import kotlinx.coroutines.*import kotlin.coroutines.resume fun fetchLater(callback: (String) -> Unit) { Thread { Thread.sleep(100); callback("data") }.start()} suspend fun fetch(): String = suspendCancellableCoroutine { cont -> fetchLater { value -> cont.resume(value) }} fun main() = runBlocking { println("before on ${Thread.currentThread().name}") println(fetch()) println("after on ${Thread.currentThread().name}")}suspendCancellableCoroutine gives you the continuation; the coroutine stays paused until resume is called, from any thread. The dispatcher then decides where the rest runs, which is why after prints on the runBlocking thread again.
What this means in practice
- Stackless. Only the state of suspended functions is kept, a small object per call, not a thread stack. That is why a hundred thousand waiting coroutines fit in memory.
- suspend is not background. Code between suspension points runs on the current thread. A CPU loop or a blocking
Thread.sleepin a suspend function blocks whatever thread the coroutine is on. - Function colour. Suspend functions can be called only from suspend code or a coroutine builder (
launch,async,runBlocking), because ordinary code has no continuation to pass. - Inline lambdas can suspend. A lambda passed to an inline function like
forEachis part of the enclosing suspend function, so you can call suspend functions inside it.
Where a coroutine runs, how it is cancelled and who waits for it are not the compiler's job: that's structured concurrency, next.
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.