Kotlin coroutine dispatchers overview

Vasiliy Nikitin
MobilePeople
Published in
5 min readApr 29, 2022

CoroutineDispatcher is a coroutine context element that dispatches a coroutines execution. It is like the threads dispatching but has some important differences. The thread is allocating processor time and its execution can be suspended at any time. But the coroutine is executed by blocks between the suspending calls. CoroutineDispatcher receives a Runnable and control, how it will be executed: when and on which thread.

Let us explain the primary coroutine dispatchers, how they work and when to use them.

Dispatchers.Default

This dispatcher works on top of a thread pool with a limited count of threads. Pool size (default and maximum threads count) is defined by environment (number of CPU cores).

It should be used for most common tasks, which should be executed in the background. Do not use it for heavy I/O tasks, which can block threads for a long time, because this thread pool has a not too large size of threads, and blocking most of them may slow down performance.

If you want to limit parallelism to a specific value, you can call limitedParallelism(parallelism: Int) on this dispatcher, that return another one.

Dispatchers.IO

This dispatcher is like Default but has a bigger parallelism limit. It uses the same thread pool under the hood but can dedicate more threads for coroutines execution. IO dispatcher, as mean its name, is useful for blocking input/output operations, but do not use it for CPU-heavy operations: large parallelism may lead to parallel work of a lot of threads, and switching between them also kills your performance.

If you want to change the coroutine dispatcher, you usually use withContext() function. This is a common way to switch dispatcher for execute a block of code inside a coroutine. But if you try to call some blocking function inside this it, for example, read a file, you may get a warning:

Inappropriate blocking method call in non-blocking context

This issue is related to a coroutine cancellation. As the documentation says, coroutine may be interrupted (canceled) only on suspension points (calling a suspend function). It means the coroutine cannot be canceled while the Java blocking function is executing (for example, Thread.sleep, reading a file, network call, etc). In Java we can interrupt thread to cancel those calls.

To resolve this issue Kotlin has a bridge between a coroutines cancellation and a thread interruption. You should use runInterruptible() instead of withContext() for blocking calls to provide an appropriate cancellation mechanism.

Dispatchers.Main

This dispatcher is used to execute code in a main (UI) thread. In the Java world, we can use Handler.post to execute something in the main thread from the background. HandlerDispatcher, an Android implementation of MainCoroutineDispatcher, do it the same way. Of course, this dispatcher does not provide parallelism, because it uses a single thread.

As was said above, on Android default implementation of the main dispatcher is HandlerDispatcher, but you can change it using Dispatchers.setMain and reset to default using Dispatchers.resetMain.

Dispatchers.Main.immediate

This is a variation of a default main dispatcher, that does not call Handler.post and execute given Runnable directly if it is called already from main thread. This is default dispatcher in LifecycleCoroutineScope and you also can use it as a default dispatcher for custom CoroutineScope.

Dispatchers.Unconfined

This dispatcher is a kind of no-op dispatcher. It literally does not dispatch a coroutine, so code may be executed in any thread, which is determined by the suspending function that was invoked.

TestDispatcher

Dispatcher for unit tests, has two implementations: StandardTestDispatcher and UnconfinedTestDispatcher. Its peculiarity is a delay-skipping behavior. This means that all delay() calls will be skipped during running on this dispatcher. The right way to use it is calling runTest function in your unit tests instead of runBlocking.

PausingDispatcher

This dispatcher is a part of AndroidX lifecycle library and allows to pause end resume coroutine execution. It using to start a coroutine when LifecycleOnwer is going to a specified state and pause it otherwise. Unfortunately, this dispatcher is not available to use it directly in your code, but you can use Lifecycle.whenStateAtLeast(minState) to run a coroutine while LifecycleOwner (fragment or activity) is on specified state.

ExecutorCoroutineDispatcher

You can create your own coroutine dispatcher that uses a specified Executor under the hood. In most case, it is a thread pool or a single thread and this approach may be useful to reuse an existing Executor for coroutines execution.

To create a ExecutorCoroutineDispatcher call asCoroutineDispatcher() extension on your executor.

Performance test

Let us compare the performance of some dispatchers: the test is sorting a list of 10,000 random numbers in parallel coroutines. This is a CPU-consuming task.

A single-thread dispatcher is created by

As you can see, a lot of parallelism is bad for most of tasks. Default dispatcher is the best choice in most cases and provides better performance.

Summary

  • Do not use Dispatchers.IO for non-io tasks. This can negatively affect the performance of the entire application due to the creation a lot of CPU-consuming threads.
  • Use runInterruptible() for blocking tasks or long-running non-suspending functions so that they can be canceled properly.
  • For calling suspend functions from unit tests use runTest instead of runBlocking. It provides correct error handling and skips delay calls.
  • Using lifecycleScope is the safest way to run a coroutine inside a fragment or activity. And note that if you calls the view inside a fragment, you should use a viewLifecycleOwner.

--

--