Kotlin Coroutines Basics

- 11 mins
Kotlin Couroutines
Banner from kotlin blog


Kotlin v1.3 was released, bringing coroutines for asynchronous programming. This article is a quick introduction to the core features of kotlinx.coroutines.

Let’s say the objective is to say hello asynchronously. Let’s start with the following classic code snippet:

fun main() {
    println("Start")

    Thread {
        Thread.sleep(3000L)
        println("Hello")
    }.start()

    println("Done")
}
Start
Done
Hello

What happened within the 3 seconds when the created Thread was sleeping? The answer: nothing! The thread was occupying memory without being used! This is when coroutines the light-weight threads kick in!

First Coroutine

The following is a basic way to migrate the previous example to use coroutines:

fun main() {
    println("Start")

    // Start a coroutine
    GlobalScope.launch {
        delay(1000L)
        println("Hello")
    }

    Thread.sleep(2000L)
    println("Done")
}
Start
Done
Hello

The coroutine is launched with launch coroutine builder in a context of a CoroutineScope (in this case GlobalScope). But what are coroutine builders? coroutine contexts? coroutine scopes?

Coroutine Builders

Coroutine builders are simple functions to create a new coroutine; the following are the main ones:

* Deffered is a generic type which extends Job.

Building Coroutines

Let’s use coroutines builders to improve the previous example by introducing runBlocking:

fun main() {
    println("Start")

    // Start a coroutine
    GlobalScope.launch {
        delay(1000L)
        println("Hello")
    }

    runBlocking {
        delay(2000L)
    }
    println("Done")
}

It is possible to do better? Yes! By moving the runBlocking to wrap the execution of the main function:

fun main() = runBlocking {
    println("Start")

    GlobalScope.launch {
        delay(1000L)
        println("Hello")
    }

    delay(2000L)
    println("Done")
}

But wait a minute, the initial goal of having delay(2000L) was to wait for the coroutine to finish! Let’s explicitly wait for it then:

fun main() = runBlocking {
    println("Start")

    val job = GlobalScope.launch {
        delay(1000L)
        println("Hello")
    }

    job.join()
    println("Done")
}

Structured concurrency

In the previous example, GlobalScope.launch has been used to create a top-level “independent” coroutine. Why “top-level”? Because GlobalScope is used to launch coroutines that are operating on the whole application lifetime. “Structured concurrency” is the mechanism providing the structure of coroutines which gives the following benefits:

Job lifecycle
Coroutine (Job) Lifecycle

Let’s apply this to our example:

fun main() = runBlocking {
    println("Start")

    // Start a coroutine (Child coroutine of runBlocking)
    launch { // or `this.launch`.
        delay(1000L)
        println("Hello")
    }
    println("Done")
}

Using the outer scope’s context

At this point, an option may be to move the inner coroutine to a function:

fun main() = runBlocking {
    println("Start")
    hello(this)
    println("Done")
}

fun hello(scope: CoroutineScope) { // or as extension function 
    scope.launch {
        delay(1000L)
        println("Hello")
    }
}

This works, but there is a more elegant way to achieve this: using suspend and coroutineScope

fun main() = runBlocking {
    println("Start")
    hello()
    println("Done")
}

suspend fun hello() = coroutineScope {
    launch {
        delay(1000L)
        println("Hello")
    }
}
[main] Start
[DefaultDispatcher-worker-1] Hello1
[main] Done

The new scope created by coroutineScope inherits the context from the outer scope.

CoroutineScope extension vs suspend

The previous example (using suspend) can be rewritten using CoroutineScope extension:

fun main() = runBlocking {
    log("Start")
    hello()  // not suspendable, no waiting!
    log("Done")
}


private fun CoroutineScope.hello() = launch(Dispatchers.Default) {
    delay(1000L)
    log("Hello1")
}
[main] Start
[main] Done
[DefaultDispatcher-worker-1] Hello1

The output is not the same! why? Here are the rules:

Coroutine Context and Dispatchers

Coroutines always execute in some CoroutineContext. The coroutine context is a set of various elements. The main elements are the Job of the coroutine and its CoroutineDispatcher.

Dispatchers

CoroutineContext includes a CoroutineDispatcher that determines what thread or threads the corresponding coroutine uses for its execution. Coroutine dispatcher can confine coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unconfined.

Coroutine builders launch, async and withContext accept a CoroutineContext parameter that can be used to explicitly specify the dispatcher for new coroutine (and other context elements).

Here are various implementations of CoroutineDispatcher:

fun main() = runBlocking {
    log("Start")
    hello()
    log("Done")
}

private suspend fun hello() = coroutineScope {
    launch {
        // context of the parent, main runBlocking coroutine
        println("[${Thread.currentThread().name}] from parent dispatcher")
    }

    launch(Dispatchers.Default) {
        // will get dispatched to DefaultDispatcher
        println("[${Thread.currentThread().name}] Dispatchers.Default")
    }

    launch(Dispatchers.IO) {
        // will get dispatched to IO
        println("[${Thread.currentThread().name}] Dispatchers.IO")
    }

    launch(Dispatchers.Unconfined) {
        // not confined -- will work with main thread
        println("[${Thread.currentThread().name}] Dispatchers.Unconfined")
    }
}
[main] Start
[DefaultDispatcher-worker-2] Dispatchers.Default
[DefaultDispatcher-worker-1] Dispatchers.IO
[main] Dispatchers.Unconfined
[main] from parent dispatcher
[main] Done

(Dispatcher.IO dispatcher shares threads with Dispatchers.Default)

Coroutine Scope

Each coroutine runs inside a scope. A scope can be application-wide or specific. Contexts and jobs lifecycles are often tied to objects who are not coroutines (Android activities for example). Managing coroutines lifecycles can be done by keeping references and handling them manually. However, a better approach is to use CoroutineScope.
The best way to create a CoroutineScope is using:

fun main() {
    println("Start")
    val activity = Activity()
    activity.doLotOfThings()
    Thread.sleep(2000)
    activity.destroy()
    println("Done")
}

private class Activity {
    private val mainScope = CoroutineScope(Dispatchers.Default)

    fun doLotOfThings() {
        mainScope.launch {
            repeat(1_000) {
                delay(400)
                doThing()
            }
        }
    }

    private fun doThing() {
        println("[${Thread.currentThread().name}] doing something...")
    }

    fun destroy() {
        mainScope.cancel()
    }
}
Start
[DefaultDispatcher-worker-1] doing something...
[DefaultDispatcher-worker-3] doing something...
[DefaultDispatcher-worker-2] doing something...
[DefaultDispatcher-worker-2] doing something...
Done

Only the first four coroutines had printed a message and the others were canceled by a single invocation of CoroutineScope.cancel() in Activity.destroy().

Alternatively, we can implement CoroutineScope interface in this Activity class, and use delegation with the default factory function:

fun main() {
    println("Start")
    val activity = Activity()
    activity.doLotOfThings()
    Thread.sleep(2000)
    activity.destroy()
    println("Done")
}

class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {

    fun doLotOfThings() {
        launch {
            repeat(1_000) {
                delay(400)
                doThing()
            }
        }
    }

    private fun doThing() {
        println("[${Thread.currentThread().name}] doing something...")
    }

    fun destroy() {
        cancel()
    }
}

Conclusion

Coroutines are a very good way to achieve asynchronous programming with kotlin.
The following is an (over)simplified diagram of coroutines structure while keeping in mind each Element is a CoroutineContext by its own:

coroutines structure
(over)simplified coroutines structure

For more advanced topics like composing suspending functions, exception handling, and supervision, the main coroutines guide is the way to go!

Tips

Sources

Mouaad Aallam

Mouaad Aallam

Software Engineer

rss facebook twitter bsky github youtube mail spotify instagram linkedin google pinterest medium vimeo mastodon gitlab docker