Coroutines Under the Hood Quiz
KOTLIN › Coroutines
What does a suspend fun compile to on the JVM?
- A method with an extra Continuation parameter, returning the value or a suspension marker
- A method annotated @Suspend that the JVM then schedules onto its own coroutine-aware runtime
- A Thread subclass whose run method contains the original body
- A regular method wrapped in a synchronized block for resumption safety
Answer: A method with an extra Continuation parameter, returning the value or a suspension marker
There is no runtime support for suspension. The compiler adds a Continuation parameter and widens the return type so the function can report that it suspended rather than finished.
Why can a suspend function only be called from another suspend function?
- Only a suspending caller has a Continuation to pass as the hidden parameter
- Because the compiler enforces it purely as a style rule with no runtime basis
- Because non-suspending callers run on threads without a dispatcher attached
- Because suspension requires a lock that only coroutine code can acquire
Answer: Only a suspending caller has a Continuation to pass as the hidden parameter
The hidden Continuation parameter has to come from somewhere, and only code already inside a coroutine has one. Builders like launch and runBlocking exist precisely to create that entry point.
Where is the state of a suspended coroutine stored?
- On the heap, in a continuation object
- On the stack of the parked thread that was running it
- In a thread-local map owned by the dispatcher
- In an off-heap arena managed by kotlinx-coroutines
Answer: On the heap, in a continuation object
Suspension is a real return, so the stack frame is gone. Keeping the state on the heap is exactly what frees the thread and makes many waiting coroutines cheap.
What is the significance of COROUTINE_SUSPENDED?
- It is the marker meaning the function did not finish, enabling a fast path when it did
- It is the exception that gets thrown when a coroutine is cancelled while mid-suspension
- It is the default value assigned to locals promoted onto the continuation
- It is the sentinel a dispatcher returns when its thread pool is saturated
Answer: It is the marker meaning the function did not finish, enabling a fast path when it did
Returning anything other than the sentinel means the value was already available, so no continuation is stored and no dispatch happens. That fast path is why suspend is nearly free when nothing actually waits.
What does this print?
- CoroutineName(worker), because a dispatcher occupies a different key
- null, because adding an element replaces the entire context
- CoroutineName(Default), derived from the newly added dispatcher
- A compile error, since two dispatchers cannot appear in one expression
Answer: CoroutineName(worker), because a dispatcher occupies a different key
A dispatcher is stored under the ContinuationInterceptor key, so adding one replaces only the dispatcher. Composition replaces per key and keeps everything else.
How does a dispatcher actually take control of thread assignment?
- It implements ContinuationInterceptor and wraps the continuation so resumption is dispatched
- It pins each coroutine to one dedicated thread and keeps it pinned there for the whole lifetime
- It rewrites suspending call sites at compile time to insert thread hops
- It sets a thread-local that each suspending call reads before executing
Answer: It implements ContinuationInterceptor and wraps the continuation so resumption is dispatched
The wrapper turns resumeWith into a dispatch of a Runnable rather than an inline call. This is also why Unconfined is not a special case: it is simply an interceptor that declines to redirect.
What does viewModelScope.launch(Job()) { } do?
- Detaches the coroutine, so clearing the ViewModel no longer cancels it
- Creates a child job underneath the enclosing scope job, exactly as normal
- Fails at runtime, since a builder rejects an explicit Job
- Starts the coroutine undispatched on the calling thread
Answer: Detaches the coroutine, so clearing the ViewModel no longer cancels it
The fresh Job replaces the scope job under the same key, so the builder finds no parent to link to. The coroutine survives onCleared and typically leaks the ViewModel.
A CoroutineStart.LAZY coroutine inside coroutineScope is never started. What happens?
- The scope waits forever, because it is an unfinished child
- It is discarded silently when the scope completes
- It is started automatically just before the scope finishes
- It throws IllegalStateException when the scope tries to complete
Answer: The scope waits forever, because it is an unfinished child
Laziness changes when the body runs, not whether the coroutine is part of the hierarchy. Structured concurrency makes the parent wait for every child, so this hangs.
What is the difference between CoroutineStart.UNDISPATCHED and Dispatchers.Unconfined?
- Undispatched controls where the body begins; unconfined controls where it resumes
- They are aliases for the same behaviour with different names
- Undispatched applies to async only, unconfined to launch only
- Undispatched skips the job hierarchy entirely, whereas unconfined skips the dispatcher
Answer: Undispatched controls where the body begins; unconfined controls where it resumes
UNDISPATCHED is a start mode affecting the first execution on the calling thread. Unconfined is a dispatcher that declines to redirect resumptions after any suspension point.
Why prefer suspendCancellableCoroutine over suspendCoroutine?
- It can be cancelled, and offers invokeOnCancellation to release the underlying resource
- It is faster, because it allocates no continuation object
- It automatically retries the callback if it never fires
- It lets the callback safely resume the continuation more than once without ever throwing
Answer: It can be cancelled, and offers invokeOnCancellation to release the underlying resource
With suspendCoroutine a cancelled coroutine waits for the callback regardless, hanging its parent and leaking the continuation if it never fires. Resuming twice throws in both variants.
What does invokeOnCancellation do when bridging an OkHttp call?
- Aborts the HTTP call so it stops consuming network after the coroutine is cancelled
- Resumes the continuation with a null response, so that the coroutine can finish early
- Re-enqueues the call once cancellation completes
- Converts the cancellation into an IOException for the caller to catch
Answer: Aborts the HTTP call so it stops consuming network after the coroutine is cancelled
Cancelling the coroutine stops your code but not the operation it started. Without this hook the request runs to completion after the screen is gone, still using radio and battery.
A suspend fun runs a tight CPU loop and is called from Dispatchers.Main. What happens?
- It blocks the main thread and may drop frames or trigger an ANR
- It is moved to a background dispatcher automatically
- It throws, because blocking work is not permitted in a suspend function
- It yields between iterations because suspend inserts cancellation checks
Answer: It blocks the main thread and may drop frames or trigger an ANR
A suspend function runs on the caller dispatcher. The keyword changes how waiting works, not where code executes, and nothing enforces the non-blocking promise.
How long does this take, roughly?
- The sum of both calls, because suspending calls in sequence run in sequence
- The longer of the two, because suspend functions run concurrently by default
- The shorter of the two, because the second cancels the first
- It depends on the dispatcher pool size at runtime
Answer: The sum of both calls, because suspending calls in sequence run in sequence
Sequential suspending calls are sequential. Overlap requires async inside a scope, then awaiting both, because the keyword only makes waiting cheap rather than creating concurrency.
Why do local variables crossing a suspension point become continuation fields?
- Because the stack frame is discarded when the function returns at that point
- To make them visible across threads without @Volatile
- To let the debugger name them in a coroutine dump
- Because the JVM forbids stack-allocated values inside generated state machine methods
Answer: Because the stack frame is discarded when the function returns at that point
Suspension is a genuine return, so the frame goes away. This is also why a long-lived coroutine capturing a large object keeps it alive: it is a field on a heap object.
Which summary of coroutines is accurate?
- Compiler-generated continuation-passing style plus a job hierarchy for lifetimes
- A green-thread runtime that schedules fibers without involving the operating system
- A more efficient thread pool implementation than ExecutorService
- A non-blocking replacement for the JVM blocking IO system calls
Answer: Compiler-generated continuation-passing style plus a job hierarchy for lifetimes
The runtime has no notion of coroutines at all. The compiler rewrites suspending code into callbacks, structured concurrency adds the Job links, and everything still runs on ordinary threads.
What does suspendCancellableCoroutine do?
- It cancels the current coroutine and resumes a different one that's been waiting in the same scope
- It suspends the coroutine until cont.resume() or cont.resumeWithException() is called, bridging callback APIs to suspend functions
- It creates a new coroutine that automatically cancels after a timeout if the result is not ready yet
- It wraps a blocking function inside a coroutine so that it runs on Dispatchers.IO rather than on the thread of the original caller
Answer: It suspends the coroutine until cont.resume() or cont.resumeWithException() is called, bridging callback APIs to suspend functions
suspendCancellableCoroutine suspends the current coroutine and gives you a CancellableContinuation. You resume it from a callback, bridging the callback world to the suspend world. invokeOnCancellation lets you clean up if the coroutine is cancelled while waiting.