Kotlin Coroutines dispatchers

An important functionality that Kotlin Coroutines library offers is letting us decide on what thread (or pool of threads) a coroutine should be running (starting and resuming). This is done using dispatchers.

In the English dictionary, a dispatcher is defined as "a person who is responsible for sending out people or vehicles where they are needed, especially emergency vehicles". In Kotlin coroutines, it is a CoroutineContext, that determines on which thread a certain coroutine will run.

Default dispatcher

If you don't set any dispatcher, the one chosen by default is Dispatchers.Default. It is designed to run CPU-intensive operations. It has a pool of threads with a size equal to the number of cores on the machine your code is running on ( but not less than two). At least theoretically, this is the optimal number of threads, assuming you are using these threads efficiently - making CPU-intensive calculations and not blocking them.

To see this dispatcher in action, run the following code:

import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlin.random.Random suspend fun main() = coroutineScope { repeat(1000) { launch { // or launch(Dispatchers.Default) { // To make it busy List(1000) { Random.nextLong() }.maxOrNull() val threadName = Thread.currentThread().name println("Running on thread: $threadName") } } }

Example result on my machine (I got 12 cores, so there are 12 threads on the pool):

Running on thread: DefaultDispatcher-worker-1
Running on thread: DefaultDispatcher-worker-5
Running on thread: DefaultDispatcher-worker-7
Running on thread: DefaultDispatcher-worker-6
Running on thread: DefaultDispatcher-worker-11
Running on thread: DefaultDispatcher-worker-2
Running on thread: DefaultDispatcher-worker-10
Running on thread: DefaultDispatcher-worker-4
...

Warning: runBlocking is setting its own dispatcher if no other is set, so inside it, the Dispatcher.Default is not the one chosen automatically.

Main dispatcher

Android and many other application frameworks have a concept of main or UI thread. That is generally the most important thread. On Android, it is the only one that can be used to interact with the UI. Because of that, it needs to be used very often, but also with great care. When the main thread is blocked, the whole application is frozen. To run a coroutine on the main thread, we use Dispatchers.Main.

Dispatchers.Main is available on Android if we import the kotlinx-coroutines-android artifact. Similarly, on JavaFX if we import kotlinx-coroutines-javafx and on Swing if we import kotlinx-coroutines-swing. There are probably some other libraries that set it. If you don't import any of them, this dispatcher is not configured, and it cannot be used.

For unit testing, you can set another dispatcher instead of this one using Dispatchers.setMain(dispatcher) from kotlinx-coroutines-test.

class SomeTest { private val dispatcher = Executors.newSingleThreadExecutor() .asCoroutineDispatcher() @Before fun setUp() { Dispatchers.setMain(dispatcher) } @After fun tearDown() { // reset main dispatcher to the original Main dispatcher Dispatchers.resetMain() dispatcher.close() } @Test fun testSomeUI() = runBlocking { launch(Dispatchers.Main) { assertEquals("UI thread", Thread.currentThread().name) } } }

The typical practice on Android is that this dispatcher is used as the default one. If you use libraries that are suspending instead of blocking, and you don't do any complex calculations, you can practically use only Dispatchers.Main. If you do some CPU-intensive operations, you should run them on the Dispatchers.Default. Those two are enough for many applications, but what if you need to block the thread? For instance, if you need to perform long I/O operations (e.g. read big files) or if you need to use some library with blocking functions. You cannot block the main thread, because your application would freeze. If you block your default dispatcher, you could risk blocking all the threads in the thread pool. Then you wouldn't be able to do any calculations. That is why we need a dispatcher for such a situation, and it is Dispatchers.IO.

IO dispatcher

Dispatchers.IO is designed to be used when we block threads with longer I/O operations. For instance, when we read files, shared preferences, or calling blocking functions. This dispatcher also has a pool of threads, but it is much bigger. Additional threads in this pool are created and are shut down on demand. The number of threads used by tasks in this dispatcher is limited by the value of "kotlinx.coroutines.io.parallelism" ([IO_PARALLELISM_PROPERTY_NAME]) system property. It defaults to the limit of 64 threads (or the number of cores if this number is larger).

This dispatcher shares threads with a Dispatchers.Default dispatcher, so using withContext(Dispatchers.IO) { ... } does not lead to an actual switching to another thread, typically execution continues in the same thread. As a result of the thread sharing, more than 64 (default parallelism) threads can be created (but not used) during operations over the IO dispatcher.

In our applications, we should rarely block threads, and so this dispatcher should not be used often. There is no need to use it when we call functions that are suspending instead of blocking.

Dispatcher with a pool of threads

There is a different situation if blocking threads might often happen. For instance, if we implement a library that uses blocking calls. If all the libraries would use IO dispatcher, those 64 threads (most likely) might all be blocked. So the best practice for libraries that are (probably) intensively blocking threads, is to define their own dispatcher with their independent pools of threads. We can do that in the following way:

val NUMBER_OF_THREADS = 20 val dispatcher = Executors.newFixedThreadPool(NUMBER_OF_THREADS) .asCoroutineDispatcher()

Such a dispatcher is used when our code might do several blocking calls. If we use it intensively, we might still have all those threads blocked, but at least those are different threads than those used by Dispatchers.IO and Dispatchers.Default. Our calls might be waiting one for another, however, the rest of the application is not affected.

Such a dispatcher is also used when we just need a huge pool of threads.

Dispatcher with a single thread

For all the dispatchers using multiple threads, we need to consider the problem of sharing the state.

import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch var i = 0 suspend fun main(): Unit = coroutineScope { repeat(10000) { launch(Dispatchers.IO) { // or Default i++ } } delay(1000) println(i) // ~9930 }

There are many ways how this problem can be solved (most will be described in the chapter "The problem with the state"), but one of the options is to use a dispatcher with just a single thread. We can make such a dispatcher by making a single-thread executor, and transforming it into a coroutine dispatcher:

val dispatcher = Executors.newSingleThreadExecutor() .asCoroutineDispatcher() // previously: // val dispatcher = newSingleThreadContext("My name")

The biggest advantage of such a dispatcher is that it does not require any synchronization. The biggest disadvantage is that due to having only one thread, it should not be blocked.

import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.concurrent.Executors var i = 0 suspend fun main(): Unit = coroutineScope { val dispatcher = Executors.newSingleThreadExecutor() .asCoroutineDispatcher() repeat(10000) { launch(dispatcher) { i++ } } delay(1000) println(i) // 10000 }

Unconfined dispatcher

The last dispatcher we need to discuss is Dispatchers.Unconfined. This dispatcher is different from the previous one, as it is not changing a thread at all. When it is started, it runs on the thread on which it was started. If it was resumed, it runs on the thread that resumed it.

It is sometimes useful for unit testing. Imagine that you need to test a function, that calls launch. Synchronizing the time might not be easy. One solution is to use Dispatchers.Unconfined instead of all other dispatchers. If it is used in all scopes, everything runs on the same thread, and we can more easily control what is the order of operations. This trick is not needed if we use runBlockingTest from kotlinx-coroutines-test. We will discuss it later in the book.

From the performance point of view, this dispatcher is the cheapest, as it never requires thread switching. So we might choose it if we do not care at all on which thread our code is running. In practice, it is not considered good to use it so recklessly. What if by accident we miss a blocking call, and we are running on the main thread on Android? This could lead to blocking the entire application.

The problem with coroutines testing library

Dispatchers are a CoroutineContext with the CoroutineInterceptor key. CoroutineInterceptor has the capability to intercept a continuation. This capability is used by dispatchers to change a thread. Although there is also a problem here - the same key is also used by the runBlockingTest from kotlinx-coroutines-test. This is why dispatchers should be injected, and not used in unit tests. We will get back to this topic in the chapter dedicated to testing.

Summary

Dispatchers determine which thread a coroutine will use for its execution.

The typical dispatchers are:

  • Dispatchers.Default, that we use for CPU-intensive operations;
  • Dispatchers.Main, that we use to access the main thread on Android, Swing, or JavaFX;
  • Dispatchers.IO, that we use when we need to do some blocking operations;
  • custom dispatcher with a pool of threads, that we use when we intensively block threads;
  • custom dispatcher with a single thread, that is used as one of the solutions to the problem of shared state;
  • Dispatchers.Unconfined, that we use when we want to have no thread switching at all.