Deep Dive into Dispatchers for Kotlin Coroutines


As we know in the previous article, Coroutine Dispatcher helps us manage threads that will be used when starting coroutines. They allow us to specify which thread or thread pool will be used.

To run a task in the background thread, we can use Dispatchers.IO or Dispatchers.Default. These two dispatchers both own thread pools that allow work to be performed in the background thread. So what is the difference between them? Let’s find out in this article.


This dispatcher’s thread pool contains a large number of threads. This allows Dispatchers.IO to perform multiple tasks in parallel. Dispatchers.IO is designed to be able to run many I/O related tasks in parallel, such as querying the database, requesting APIs, reading and writing files. You can see that these tasks can take time to complete, but they don’t consume too much CPU resources. Therefore, Dispatchers.IO’s thread pool always has a large number of threads, ready to run in parallel, helping tasks to be completed faster.
Some real cases where we use Dispatchers.IO:
– Rest API calls
– Database queries
– Upload/Download files
– Read/Write files
Example of how to use Dispatchers.IO

suspend fun fetchData(): String {

 lifecycleScope.launch (Dispatchers.IO) {

   val response = networkRequest()

   // parse response into a String





This is the Dispatcher used by default when you do not specify any Dispatcher when starting the coroutine. Its thread pool contains a finite number of threads, equal to the number of CPU cores of the device. This dispatcher is designed to perform computational or CPU-intensive tasks. Some cases where you can use Dispatchers.Default:
– Search, sort, filter elements in a large list
– Encrypt/Decrypt large data
– Parse a large JSON/XML file
– Transmit/Receive large data

Threads in the thread pool are designed to run in parallel effectively, helping tasks that consume CPU resources to be completed faster.
Dispatchers.Default example:

suspend fun joinImagesData(): String {

 lifecycleScope.launch (Dispatchers.Default) {

   val bigImages: List<Bitmap> = getImagesFromStorate()

   // parse response into a String

   val result: Bitmap = joinImages(bigImages)





This is a pretty special dispatcher. Initially, when you start a coroutine with this Dispatcher, it will use the caller thread to handle tasks. This thread will be used until a suspend function appears and completes. Starting from this point, the thread used to execute the above suspend function will be used to execute the next tasks.
Let’s look at the following example
First we define the logs function to print data attached to the thread.

object Utils {

 fun log(message: String) {

   Log.d("CoroutineExample", "[thread info: ${Thread.currentThread().name}] $message")



We define two more suspend functions as follows:

suspend fun useMainThread(): String {

 return withContext(Dispatchers.Main) {




You can see this suspend function uses the main thread to execute the task.

suspend fun useBackgroundThread(): String {

 return withContext(Dispatchers.Default) {




You can see this suspend function uses a background thread to execute the task
And finally, we run the following:

GlobalScope.launch (Dispatchers.Unconfined) {

 Utils.log("Dispatcher Unconfined: Start")

 val s = myFunction2()


 Utils.log("After background suspend function")


 Utils.log("After main suspend function")


We get the result:

[thread info: main] Dispatcher Unconfined: Start

[thread info: DefaultDispatcher-worker-1] After background suspend function

[thread info: main] After main suspend function

You can see, after each suspend is completed, the thread that Dispatchers.Unconfined has changed.


So this article has presented the differences between 3 Dispatchers in coroutine. Hope it will be useful to you. See you again in the following articles.

Leave a Reply

Your email address will not be published. Required fields are marked *