Dispatchers & Threading Quiz

KOTLIN › Coroutines

While a coroutine on Dispatchers.IO is suspended at delay(1000), what happens to its IO thread?

Answer: It returns to the pool and is free to run other coroutines during the wait

Suspension releases the thread. Holding it would make coroutines no cheaper than blocking calls, which is the entire problem they exist to solve.

How are Dispatchers.IO and Dispatchers.Default related in modern kotlinx.coroutines?

Answer: They share one thread pool but have different concurrency ceilings

One shared pool, two limits: Default is capped at core count, IO at 64. That is why switching between them is often cheap, and why the 64 is an application-wide ceiling rather than a per-call-site one.

You need to cap your app at three concurrent image uploads. What is the idiomatic tool?

Answer: Dispatchers.IO.limitedParallelism(3), used for every upload

limitedParallelism gives exactly an N-at-a-time ceiling with no threads to own or close. A Mutex would serialise to one at a time, a custom pool leaks if unclosed, and Default caps at core count rather than three.

What does Dispatchers.Default.limitedParallelism(1) buy you for shared mutable state?

Answer: Serialised execution, so the state needs no lock, atomic, or @Volatile

One coroutine runs at a time, and the dispatch that moves work between threads supplies the happens-before edge. That is thread confinement, and it removes the need for synchronisation as long as the state never escapes.

What is printed first here?

Answer: A, because Unconfined runs the body inline before launch returns

Unconfined does not dispatch. It executes the body immediately on the calling thread up to the first suspension, and with no suspension at all the whole body runs before launch returns.

You are already on the main thread and call withContext(Dispatchers.Main) { }. What happens?

Answer: The block is posted to the main Looper and runs on a later queue pass

Plain Main always posts. Main.immediate is the variant that checks whether it is already on the right thread and skips the message-queue round trip, which is why lifecycle collection helpers use it.

Which dispatcher fits parsing a large JSON payload already held in memory?

Answer: Dispatchers.Default, whose pool is sized for CPU-bound work

Nothing blocks here: it is pure computation, so it belongs on the pool capped at core count. Running it on IO wastes a pool built for waiting, and running it on Main risks a dropped frame.

A repository function suspends but never blocks, only calling other suspend functions. Should it use withContext(Dispatchers.IO)?

Answer: No: it blocks nothing, so it is already main-safe and the switch costs a round trip

Main-safety is a promise not to block the calling thread, and a function that only awaits other suspend functions already keeps it. Blanket withContext wrapping is the most common coroutine smell in real codebases.

What is the practical risk of an executor-backed dispatcher over limitedParallelism?

Answer: It owns real threads that leak for the life of the process if never closed

ExecutorCoroutineDispatcher holds actual threads until close() is called, and it sits outside the shared pool so those threads idle when unused. limitedParallelism borrows instead of allocating, so there is nothing to leak.

Why does viewModelScope require Dispatchers.setMain() in tests rather than constructor injection?

Answer: It hard-codes Dispatchers.Main internally, so no parameter of yours reaches it

The scope is built inside the lifecycle library with a fixed Main dispatcher. setMain swaps the platform main dispatcher globally, which is the only injection point available.

What is wrong with this batch save?

Answer: It pays a dispatcher round trip per row instead of switching once for the batch

Each withContext builds a context, may hand off threads, suspends, and dispatches back. Hoist the switch outside the loop so the whole batch pays it once.

On Android, what backs Dispatchers.Main?

Answer: A Handler bound to the main Looper, so each dispatch is a posted message

The kotlinx-coroutines-android artifact installs a Handler-backed dispatcher, which is why Main inherits the message queue ordering and why coroutines on Main compete with touch events and draw calls.

Where does Dispatchers.Unconfined legitimately belong?

Answer: Tests and framework code that only forwards a value and does not care where it lands

Its thread after the first suspension is undefined, which rules out UI work. In application code, wanting inline execution on the main thread means you wanted Main.immediate, which has a defined thread.

What does the shared 64-thread ceiling on Dispatchers.IO mean in practice?

Answer: It is application-wide, so a 65th concurrent blocking call queues behind the others

One ceiling for the whole process. Launching more blocking work than that does not create more threads, it just queues, which is why a subsystem that must not starve others should take its own limitedParallelism slice.

Why inject a dispatcher as a constructor parameter rather than referencing Dispatchers.IO directly?

Answer: So a test can substitute one driven by virtual time and assert deterministically

A hard-coded dispatcher means the test races a real thread pool, and people paper over it with arbitrary delays. Injection lets advanceUntilIdle() finish the work deterministically.

What is the primary purpose of withContext(Dispatchers.IO) inside a suspend function?

Answer: To switch the block onto an I/O thread, keeping the function main-safe

withContext switches to the specified dispatcher for the block and returns to the original context afterward, so blocking I/O runs off the main thread.

What does the + operator do when combining two CoroutineContext values?

Answer: It merges them: for duplicate keys the right-side element wins, non-overlapping keys are kept from both sides

The + operator merges two contexts like maps. If both contain the same key (e.g. both have a dispatcher), the right side wins. Elements with different keys (e.g. a dispatcher and a CoroutineName) are both preserved.

What is the purpose of CoroutineName in the coroutine context?

Answer: It names the coroutine for debugging: the name appears in thread names, stack traces, and logging when the debug agent is enabled

CoroutineName is a debugging aid. When you enable -Dkotlinx.coroutines.debug, the name appears in thread names (e.g. "DefaultDispatcher-worker-1 @fetch-user#3") and in exception messages, making it much easier to trace which coroutine failed.

Back to Dispatchers & Threading