Kotlin coroutine dispatchers overview
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 ofrunBlocking
. It provides correct error handling and skipsdelay
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 aviewLifecycleOwner
.