What is a CoroutineDispatcher, and when would you use Dispatchers.Default versus Dispatchers.IO?
answer
- Default = CPU-bound, pool ~= core count
- IO = blocking I/O, up to 64 threads
- Switch with withContext
- IO and Default share threads
- Wrong dispatcher -> thread starvation
basics
~10 sA dispatcher decides which thread a coroutine runs on. Use Default for heavy CPU work like sorting or parsing, and IO for waiting on network or disk calls.
solid answer
~40 sA CoroutineDispatcher is the CoroutineContext element that controls which thread (or thread pool) a coroutine and its resumptions run on. Dispatchers.Default is backed by a shared pool sized to the number of CPU cores, ideal for CPU-bound work (sorting, JSON parsing, hashing). Dispatchers.IO is backed by a larger, elastic pool (default up to 64 threads or cores, whichever is higher) tuned for blocking I/O such as JDBC calls, file reads, or blocking HTTP clients, where threads spend time parked rather than computing. You switch dispatchers with withContext(Dispatchers.IO) { ... }. Choosing IO for blocking calls prevents starving the small Default pool; choosing Default for CPU work avoids spawning excess threads. Both share threads under the hood, so switching between them is cheap and may not require an actual thread hand-off.
code
kotlin · 4 linessuspend fun report(id: Long): String {
val rows = withContext(Dispatchers.IO) { db.fetchRows(id) } // blocking I/O
return withContext(Dispatchers.Default) { rows.summarize() } // CPU work
}go deeper
Knows Default = CPU, IO = blocking calls, and that dispatcher picks the thread.
Explains pool sizing rationale and uses withContext correctly to switch.
Articulates thread starvation risk and that IO/Default share threads, making switches cheap.
Reasons about pool capacity tuning, parallelism limits, and system-wide throughput trade-offs.
## What a dispatcher is A **CoroutineDispatcher** is an element of the **CoroutineContext** that determines *which thread or thread pool* a coroutine uses when it starts and every time it **resumes** after a suspension point. It is essentially a strategy for scheduling work onto threads. You combine it into a scope or `withContext` call: `withContext(Dispatchers.IO) { ... }`. ## The standard dispatchers - **Dispatchers.Default** — a shared thread pool whose size equals the number of CPU cores (minimum 2). Built for **CPU-bound** work that keeps a core busy: sorting large lists, parsing/serializing, image processing, cryptographic hashing. More threads than cores would only cause context-switch overhead, so the pool is deliberately small. - **Dispatchers.IO** — a pool designed for **blocking I/O**: reading files, JDBC/database calls, blocking HTTP clients, `Thread.sleep`-style waits. It allows up to 64 threads by default (or the core count if higher) because such threads spend most of their time **parked/waiting**, not computing. Using IO keeps blocking calls off the small Default pool. ## Why the distinction matters If you run a blocking JDBC call on `Dispatchers.Default`, you can occupy all CPU-sized threads with parked work and **starve** genuine CPU tasks — throughput collapses. Conversely, doing pure computation on IO wastes nothing but offers no benefit. Match the dispatcher to the work's nature. ## Shared threads `Dispatchers.IO` and `Dispatchers.Default` **share the same underlying threads**. Switching from Default to IO via `withContext` may reuse the current thread without a real hand-off, making the switch cheap. IO is conceptually a *view* over the shared pool with a higher parallelism cap. ```kotlin suspend fun loadUser(id: Long): User = withContext(Dispatchers.IO) { // blocking JDBC call belongs on IO jdbcTemplate.queryForObject("...", ...) } suspend fun crunch(data: List<Int>): Long = withContext(Dispatchers.Default) { // CPU-bound aggregation belongs on Default data.fold(0L) { acc, x -> acc + heavyTransform(x) } } ``` ## Key APIs - `Dispatchers.Default`, `Dispatchers.IO` — the dispatcher objects. - `withContext(dispatcher) { ... }` — suspendingly switches the dispatcher for a block. - `suspend` — marks functions that may suspend; dispatchers control where they resume.
- Why not just run everything on Dispatchers.IO?CPU-bound work on IO can spin up many threads competing for the same cores, causing context-switch overhead. Default's core-sized pool maximizes CPU throughput.
- Does withContext always switch threads?No. If the target dispatcher shares the current thread (e.g., Default->IO) and capacity allows, it may resume on the same thread; the switch is logical, not always physical.
Default is a small expert crew kept busy; IO is a big call-center floor where most agents are on hold waiting.
saying these in an interview costs you the question
- Saying IO has unlimited threads
- Claiming Default is for I/O
- Thinking each dispatcher has a fully separate thread pool
- Running blocking JDBC on Dispatchers.Default
- Confusing dispatcher with launch/async builder