How to use coroutine Dispatchers in Kotlin

dimitrilc 3 Tallied Votes 145 Views Share

Introduction

With coroutines in Kotlin, we are able to execute suspending functions without blocking the current thread. By default, most coroutine builder functions use the Dispatchers.Default context, but it is also possible to use other implementations of CoroutineContext.

In this tutorial, we will learn about the 2 CoroutineDispatchers: Default and IO.

Goals

At the end of the tutorial, you would have learned:

  1. The difference between:
    a. Dispatchers.Default.
    b. Dispatchers.IO.
  2. How to use them.

Prerequisite Knowledge

  1. Basic Kotlin.
  2. Basic Coroutines.

Tools Required

  1. A Kotlin IDE such as IntelliJ Community Edition.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a Kotlin project using Gradle 7.2 as the build tool and Kotlin DSL as configuration file.

  2. Copy and paste the content below into your build.gradle.kts.

     import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
    
     plugins {
        kotlin("jvm") version "1.5.31"
     }
    
     group = "com.example"
     version = "1.0-SNAPSHOT"
    
     repositories {
        mavenCentral()
     }
    
     dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
        testImplementation(kotlin("test"))
     }
    
     tasks.test {
        useJUnitPlatform()
     }
    
     tasks.withType<KotlinCompile>() {
        kotlinOptions.jvmTarget = "16"
     }
  3. The project uses the latest Kotlin and coroutines version as of this writing.

  4. Create a new package under src/main/kotlin called com.example.

  5. Create a new Kotlin file called Entry.

  6. Copy and paste the code below into Entry.kt.

     package com.example
    
     import kotlinx.coroutines.*
     import kotlin.coroutines.*
    
     fun main() = runBlocking { //1
    
        println(this) //2
        println(this.coroutineContext) //3
        println("ContinuationInterceptor: ${this.coroutineContext[ContinuationInterceptor]}") //4
    
        launch { //5
            doBlocking()
        }
    
        this.launch(
            EmptyCoroutineContext, // 6
            CoroutineStart.DEFAULT,
            { doBlocking() }
        )
     //
     //    launch(Dispatchers.Default){ //7
     //        doBlocking()
     //    }
    
     //    repeat(20){ //8
     //        launch(Dispatchers.IO){
     //            doIO()
     //        }
     //    }
    
     //    repeat(20){ //9
     //        launch(Dispatchers.Default){
     //            doBlocking()
     //        }
     //    }
    
        println()
     }
    
     suspend fun doBlocking() { //10
        println("start doBlocking task in thread: ${Thread.currentThread().name}") //11
        delay(2000L) //12
        println("end doBlocking task in thread: ${Thread.currentThread().name}") //13
     }
    
     suspend fun doIO(){ //14
        //Pretend that we are doing some blocking IO task.
        println("start IO task in thread: ${Thread.currentThread().name}") //15
        delay(2000L) //15
        println("end IO task in thread: ${Thread.currentThread().name}") //16
     }

EmptyCoroutineContext

If we look at their signatures of the common coroutine builders such as launch() or async(),

fun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit): Job

fun <T> CoroutineScope.async(context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T): Deferred<T>

we can see that they both use EmptyCoroutineContext by default. They are also extension functions of CoroutineScope, so if a specific CoroutineContext is not specified, they will use any CoroutineDispatcher or ContinuationInterceptor from the calling parent context(the receiver object) to decide how to run their lambdas.

To understand how this works, we have to examine the code inside Entry.kt.

    launch { //5
       doBlocking() 
    }

    this.launch(
       EmptyCoroutineContext, // 6
       CoroutineStart.DEFAULT,
       { doBlocking() }
    )

The two coroutine builders at line 5 and 6, though different at call-site code, are exactly the same. Line 5 is the version with all of the syntax sugar while line 6 specifies everything out to make the code easy to understand. The this keyword references the CoroutineScope inherited from the outer coroutine (a BlockingCoroutine, which is a private Kotlin class).

When these coroutines are executed, they will still run within their own scope(and not BlockingCoroutine’s), but the decision about which thread to run on is still determined by BlockingCoroutine’s CoroutineContext.

At line 1, we specify that everything inside main() must run inside the runBlocking() scope builder. But notice the difference between the signature of runBlocking() and launch()(above).

fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

runBlocking() is NOT an extension function, so it is not inheriting any parent CoroutineScope.

If a CoroutineContext is not specified at the call-site, then runBlocking creates a new CoroutineContext (tied to the GlobalScope) combined with an EventLoop. This CoroutineContext is then used to create a BlockingCoroutine. The lambda passed to runBlocking() will be executed within BlockingCoroutine’s.

It is okay if you did not understand the previous paragraph. You only have to know that, when a CoroutineContext is not specified, runBlocking() uses a special CoroutineContext, whose implementation is internal to Kotlin and should not be used in general code. It is this special default CoroutineContext that exposes a ContinuationInterceptor to the child coroutines(lines 5 and 6).

If we run the code we have so far as-is, we can see the this CoroutineScope, the coroutineContext, and the ContinuationInterceptor.

    BlockingCoroutine{Active}@2437c6dc
    [BlockingCoroutine{Active}@2437c6dc, BlockingEventLoop@7b1d7fff]
    ContinuationInterceptor: BlockingEventLoop@7b1d7fff

Because the launch() coroutines at lines 5 and 6 inherit (and use) the special CoroutineContext, they will NOT use Dispatchers.Default. They are confined to run on the main thread.

    start doBlocking task in thread: main
    start doBlocking task in thread: main
    end doBlocking task in thread: main
    end doBlocking task in thread: main

If we do not want launch() builders to run on the main thread, we must use different Dispatcher(s).

Dispatchers.Default

Now that we have understood how coroutines inside runBlocking() work, we can comment out the first 3 println() calls and the whole coroutines at line 5 and 6.

Uncomment the coroutine at line 7.

At line 7, we passed in Dispatchers.Default, which will tell this coroutine to run on a shared pool of threads on the JVM. If we run this code, we will see that the coroutines are no longer running on the main thread, but a DefaultDispatcher-worker thread.

    start doBlocking task in thread: DefaultDispatcher-worker-1
    end doBlocking task in thread: DefaultDispatcher-worker-1

Because the number of threads that Dispatchers.Default uses are maxed out by the number of CPU cores (minimum of 2 threads), it makes sense to use it for CPU-bound tasks(CPU-bound tasks don’t spend a lot of time waiting).

Dispatchers.IO

Dispatchers.IO works similarly to Dispatchers.Default, but with a few differences. The maximum number of threads that it can use is 64. Here the number of maximum threads can be higher than the number of CPU cores because Kotlin assumes that these tasks are mostly blocking tasks anyways, which should be suspended to free up the CPU to do something else.

Comment out the coroutine at line 7 and uncomment the block at line 8.

Line 9 launches 20 different coroutines and uses Dispatchers.IO. When we run this code, we can clearly see that the number of threads is higher than 8 (or a different number depends on your computer’s amount of CPU cores; I have 8 virtual cores).

My output is:

    start IO task in thread: DefaultDispatcher-worker-1
    start IO task in thread: DefaultDispatcher-worker-3
    start IO task in thread: DefaultDispatcher-worker-2
    start IO task in thread: DefaultDispatcher-worker-4
    start IO task in thread: DefaultDispatcher-worker-6
    start IO task in thread: DefaultDispatcher-worker-7
    start IO task in thread: DefaultDispatcher-worker-5
    start IO task in thread: DefaultDispatcher-worker-9
    start IO task in thread: DefaultDispatcher-worker-8
    start IO task in thread: DefaultDispatcher-worker-10
    start IO task in thread: DefaultDispatcher-worker-12
    start IO task in thread: DefaultDispatcher-worker-14
    start IO task in thread: DefaultDispatcher-worker-11
    start IO task in thread: DefaultDispatcher-worker-16
    start IO task in thread: DefaultDispatcher-worker-13
    start IO task in thread: DefaultDispatcher-worker-15
    start IO task in thread: DefaultDispatcher-worker-18
    start IO task in thread: DefaultDispatcher-worker-19

    start IO task in thread: DefaultDispatcher-worker-23
    start IO task in thread: DefaultDispatcher-worker-24
    end IO task in thread: DefaultDispatcher-worker-9
    end IO task in thread: DefaultDispatcher-worker-15
    end IO task in thread: DefaultDispatcher-worker-3
    end IO task in thread: DefaultDispatcher-worker-9
    end IO task in thread: DefaultDispatcher-worker-2
    end IO task in thread: DefaultDispatcher-worker-2
    end IO task in thread: DefaultDispatcher-worker-22
    end IO task in thread: DefaultDispatcher-worker-14
    end IO task in thread: DefaultDispatcher-worker-7
    end IO task in thread: DefaultDispatcher-worker-11
    end IO task in thread: DefaultDispatcher-worker-10
    end IO task in thread: DefaultDispatcher-worker-16
    end IO task in thread: DefaultDispatcher-worker-15
    end IO task in thread: DefaultDispatcher-worker-24
    end IO task in thread: DefaultDispatcher-worker-1
    end IO task in thread: DefaultDispatcher-worker-3
    end IO task in thread: DefaultDispatcher-worker-4
    end IO task in thread: DefaultDispatcher-worker-8
    end IO task in thread: DefaultDispatcher-worker-19
    end IO task in thread: DefaultDispatcher-worker-6

We can clearly see the difference between Dispatchers.IO and Dispatchers.Default when we comment out line 8 (block) and uncomment line 9 (block). Even though we launched 20 coroutines, they are restricted to only use workers 1 to 8.

    start doBlocking task in thread: DefaultDispatcher-worker-1
    start doBlocking task in thread: DefaultDispatcher-worker-2
    start doBlocking task in thread: DefaultDispatcher-worker-3
    start doBlocking task in thread: DefaultDispatcher-worker-4
    start doBlocking task in thread: DefaultDispatcher-worker-5
    start doBlocking task in thread: DefaultDispatcher-worker-6
    start doBlocking task in thread: DefaultDispatcher-worker-7
    start doBlocking task in thread: DefaultDispatcher-worker-8

    start doBlocking task in thread: DefaultDispatcher-worker-2
    start doBlocking task in thread: DefaultDispatcher-worker-3
    start doBlocking task in thread: DefaultDispatcher-worker-1
    start doBlocking task in thread: DefaultDispatcher-worker-7
    start doBlocking task in thread: DefaultDispatcher-worker-6
    start doBlocking task in thread: DefaultDispatcher-worker-4
    start doBlocking task in thread: DefaultDispatcher-worker-7
    start doBlocking task in thread: DefaultDispatcher-worker-1
    start doBlocking task in thread: DefaultDispatcher-worker-5
    start doBlocking task in thread: DefaultDispatcher-worker-8
    start doBlocking task in thread: DefaultDispatcher-worker-3
    start doBlocking task in thread: DefaultDispatcher-worker-2
    end doBlocking task in thread: DefaultDispatcher-worker-7
    end doBlocking task in thread: DefaultDispatcher-worker-2
    end doBlocking task in thread: DefaultDispatcher-worker-3
    end doBlocking task in thread: DefaultDispatcher-worker-2
    end doBlocking task in thread: DefaultDispatcher-worker-8
    end doBlocking task in thread: DefaultDispatcher-worker-1
    end doBlocking task in thread: DefaultDispatcher-worker-8
    end doBlocking task in thread: DefaultDispatcher-worker-5
    end doBlocking task in thread: DefaultDispatcher-worker-8
    end doBlocking task in thread: DefaultDispatcher-worker-4
    end doBlocking task in thread: DefaultDispatcher-worker-4
    end doBlocking task in thread: DefaultDispatcher-worker-8
    end doBlocking task in thread: DefaultDispatcher-worker-5
    end doBlocking task in thread: DefaultDispatcher-worker-1
    end doBlocking task in thread: DefaultDispatcher-worker-3
    end doBlocking task in thread: DefaultDispatcher-worker-2
    end doBlocking task in thread: DefaultDispatcher-worker-7
    end doBlocking task in thread: DefaultDispatcher-worker-6
    end doBlocking task in thread: DefaultDispatcher-worker-8
    end doBlocking task in thread: DefaultDispatcher-worker-4

Summary

We have learned how to use Dispatchers.Default and Dispatchers.IO in this tutorial. The full project code can be found here: https://github.com/dmitrilc/DaniwebCoroutineLaunch