Interview QuestionPractical QuestionFollow-up Questions

How Coroutine Dispatchers Enable Thread Switching

skydovesJaewoong Eum (skydoves)||10 min read

How Coroutine Dispatchers Enable Thread Switching

Coroutine dispatchers are the mechanism through which Kotlin Coroutines control which thread or thread pool a coroutine executes on. They achieve this by implementing the ContinuationInterceptor interface, which allows them to wrap every continuation with thread-switching logic before it is resumed. Understanding the internal machinery of dispatchers, from CoroutineDispatcher.dispatch() through DispatchedContinuation and DispatchedTask, reveals how coroutines seamlessly move between threads without the developer writing any explicit threading code. By the end of this lesson, you will be able to:

  • Explain how CoroutineDispatcher implements ContinuationInterceptor to intercept and redirect continuations.
  • Describe the role of DispatchedContinuation as the bridge between dispatchers and the continuation machinery.
  • Trace the execution path through DispatchedTask.run() and explain the prompt cancellation guarantee.
  • Identify the purpose of each resume mode and how it affects dispatch and cancellation behavior.
  • Apply knowledge of dispatcher internals to reason about Dispatchers.Default, Dispatchers.IO, Dispatchers.Main, and Dispatchers.Unconfined.

The CoroutineDispatcher Base Class

Every dispatcher in Kotlin Coroutines extends CoroutineDispatcher, which serves as both a coroutine context element and a continuation interceptor:

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {

    public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true

    public abstract fun dispatch(context: CoroutineContext, block: Runnable)

    public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
        DispatchedContinuation(this, continuation)
}

The class carries three responsibilities that together form the dispatching pipeline.

isDispatchNeeded() returns true by default, meaning the dispatcher will always schedule work onto its target thread or pool. Two dispatchers override this: Dispatchers.Unconfined always returns false because it runs continuations on whatever thread resumed them, and Dispatchers.Main.immediate returns false when the caller is already on the main thread to avoid a redundant round-trip through the event loop.

dispatch() is the abstract core. Each concrete dispatcher implements this to schedule a Runnable onto its execution target. The Runnable that gets dispatched is the DispatchedContinuation itself, since it extends DispatchedTask, which extends SchedulerTask, which implements Runnable.

interceptContinuation() is marked final and is the entry point into the dispatch mechanism. When the coroutine runtime creates a continuation, it checks the context for a ContinuationInterceptor. If one is found, it calls interceptContinuation(), which wraps the original continuation in a DispatchedContinuation. From that point forward, every resumeWith() call goes through the dispatching layer.

DispatchedContinuation: The Thread-Switching Wrapper

DispatchedContinuation is the class that bridges the gap between the coroutine continuation and the dispatcher's thread pool. It holds a reference to both the dispatcher and the original continuation, and it delegates the Continuation interface to the original while overriding resumeWith() to inject dispatching logic:

internal class DispatchedContinuation<in T>(
    @JvmField internal val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {

    override fun resumeWith(result: Result<T>) {
        val state = result.toState()
        if (dispatcher.safeIsDispatchNeeded(context)) {
            _state = state
            resumeMode = MODE_ATOMIC
            dispatcher.safeDispatch(context, this)
        } else {
            executeUnconfined(state, MODE_ATOMIC) {
                withCoroutineContext(context, countOrElement) {
                    continuation.resumeWith(result)
                }
            }
        }
    }
}

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor