How do you build a custom CoroutineDispatcher from a java.util.concurrent.Executor, and what must you manage about its lifecycle?
answer
- asCoroutineDispatcher() wraps an Executor
- ExecutorCoroutineDispatcher is Closeable
- You must call close() -> else thread leak
- close() shuts down the underlying ExecutorService
- Prefer standard dispatchers unless bounded/custom needed
basics
~10 sWrap an existing Java thread pool with asCoroutineDispatcher() to get a dispatcher coroutines can use. Because you created the pool, you are responsible for shutting it down when done.
solid answer
~40 sUse the extension Executor.asCoroutineDispatcher() (or ExecutorService.asCoroutineDispatcher()) to adapt any java.util.concurrent.Executor into a CoroutineDispatcher, letting coroutines schedule onto your own pool (e.g., a fixed pool for a bounded resource, or one wrapping a legacy framework executor). The returned ExecutorCoroutineDispatcher implements Closeable. You own the lifecycle: call .close() to shut it down, which also shuts down the underlying ExecutorService when you used the ExecutorService overload. Forgetting to close leaks threads. For a one-off, single-thread dispatcher there used to be newSingleThreadContext / newFixedThreadPoolContext, but these are delicate (you must close them) and are generally discouraged in favor of limitedParallelism on a shared dispatcher. Custom executors are warranted when you need bounded parallelism tied to an external resource, custom thread naming/priorities, or integration with an existing executor; otherwise prefer the standard dispatchers.
code
kotlin · 7 linesval exec = Executors.newFixedThreadPool(2)
val disp = exec.asCoroutineDispatcher()
try {
repeat(10) { i -> launch(disp) { handle(i) } }.join()
} finally {
disp.close() // releases threads; also shuts down 'exec'
}go deeper
Knows asCoroutineDispatcher() turns a Java executor into a dispatcher.
Uses the wrapper correctly and knows close() is required to avoid leaks.
Explains Closeable ownership, ExecutorService shutdown semantics, and when a custom pool is justified.
Weighs custom executors vs limitedParallelism, daemon/thread-naming policy, and resource-bounding strategy across the system.
## Adapting an Executor Kotlin provides extension functions to turn a JDK **Executor** into a dispatcher: - `Executor.asCoroutineDispatcher(): CoroutineDispatcher` - `ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher` This lets coroutines schedule their resumptions onto **your** thread pool — useful for a bounded resource (a fixed pool sized to a connection limit), custom thread names/priorities, or bridging a framework's existing executor into coroutine code. ```kotlin val dispatcher = Executors .newFixedThreadPool(4) { r -> Thread(r, "db-pool").apply { isDaemon = true } } .asCoroutineDispatcher() try { withContext(dispatcher) { blockingDbCall() } } finally { dispatcher.close() // shuts down the wrapped ExecutorService } ``` ## Lifecycle ownership — the crucial part The object returned by the **ExecutorService** overload is an **ExecutorCoroutineDispatcher**, which is **Closeable**. Because *you* created the executor, *you* must release it: - Call `dispatcher.close()` when finished. For the ExecutorService overload, `close()` also calls `shutdown()` on the underlying service. - Failing to close **leaks threads** (and may keep the JVM alive if threads aren't daemons). - Don't close a dispatcher you don't own (e.g., never close `Dispatchers.IO`). - A closed dispatcher rejects new tasks; using it afterward throws. ## newSingleThreadContext / newFixedThreadPoolContext These factory functions create a private dispatcher backed by dedicated threads. They are convenient but: - They allocate **real OS threads** you must `close()`. - They are generally **discouraged**; the modern idiom is `Dispatchers.Default.limitedParallelism(n)` or `Dispatchers.IO.limitedParallelism(n)` to get bounded parallelism **without** dedicated threads (covered by the sibling leaf — not the focus here). ## When to use a custom executor Reach for `asCoroutineDispatcher` when you need: a pool sized to an **external resource limit**, specific **thread naming/priority/daemon** flags, or to reuse an **existing framework executor**. For ordinary CPU or I/O work, prefer the built-in `Dispatchers.Default` / `Dispatchers.IO`. ## Key APIs / terms - `Executor.asCoroutineDispatcher()`, `ExecutorService.asCoroutineDispatcher()` - `ExecutorCoroutineDispatcher` (a `CoroutineDispatcher` that is `Closeable`) - `.close()` — releases threads / shuts down the executor - `Executors.newFixedThreadPool`, custom `ThreadFactory`
- What happens if you forget to close an ExecutorCoroutineDispatcher?The underlying threads stay alive, leaking resources; non-daemon threads can also prevent JVM shutdown.
- Why prefer limitedParallelism over newFixedThreadPoolContext for bounded concurrency?limitedParallelism caps parallelism on a shared pool without allocating dedicated OS threads or requiring manual close, so it's cheaper and safer.
- Is it safe to call close() on Dispatchers.IO?No. You don't own the shared dispatchers; closing them is unsupported and would break the rest of the app.
It's like renting your own truck instead of using the shared fleet: handy for special cargo, but you must return it or keep paying.
saying these in an interview costs you the question
- Wrapping an executor but never closing the dispatcher
- Closing a shared dispatcher like Dispatchers.IO
- Thinking asCoroutineDispatcher creates a brand-new pool itself
- Using newSingleThreadContext without close()
- Reaching for custom executors when Default/IO would do