Kotlin Coroutines
February 1, 2019  |  

Coroutines have been available since Kotlin 1.1 but Kotlin 1.3 is the first Kotlin release where coroutines are part of the stable API which means they’re finally ready for prime time. They are a good tool for writing and maintaining asynchronous code without losing your sanity.

What are Coroutines?

Every seasoned programmer knows that threads are evil. You should think twice before using threads in your code. Threads are powerful, but they come with terrible costs. The code that uses threads is generally error-prone and hard to maintain.

Coroutines allow us to write concurrent code without using threads explicitly. The main purpose of coroutines is to reduce the costs of concurrent code while keeping its main benefits. Those costs are reduced by making coroutine-enabled code more readable and almost indistinguishable from the vanilla Kotlin.

The terms “coroutines” and “suspending functions” are often used interchangeably in Kotlin. In fact, suspending functions is the real “magic” behind coroutines.

Coroutines in Action

Theory is important, but it’s nice to see a practical example before committing your time to learn something new. Maybe this fancy tool isn’t a good fit for your particular tasks? Let’s look at the following function:

fun displayBooks() {
    progressBar.show()
    loadBooks()
    progressBar.hide()
}

If you’re familiar with UI programming, it’s very easy to see the problem with this code. The loadBooks() function can take an unpredictable amount of time, and most UI toolkits are single-threaded, so the UI thread will get blocked. Chances are, we won’t even see that progress bar. Here is the flowchart which shows what this code really does:

The “load data” stage blocks the UI and makes it unresponsive. It means that users won’t see any animations and that they won’t be able to press any buttons or interact with anything UI related. The thread is busy, nothing can be done until it completes all three of those steps. That’s not what we want and there must be a way to do it right.

Well, that’s where threads shine. We can execute loadBooks() in a separate thread in order to unblock the UI thread and make our program responsive:

fun displayBooks() {
    progressBar.show()

    thread { 
        loadBooks()    
    }
    
    progressBar.hide()
}

It’s a bit better, but it still won’t have the desired effect. The problem is: there is no guarantee that progressBar.hide() will be called after loadBooks(). The extra thread that we introduced is now messing with the execution order. Let’s look at this chart:

When we read the code top to bottom, it looks like it should complete all the steps in the order they were declared:

progressBar.show()
loadBooks()
progressBar.hide()

In fact, the program will to this:

progressBar.show()
progressBar.hide()
loadBooks()

As anything thread-related, it looks like a trivial task but ends up being a huge pain in the ass. We tend to read the code like a book, and the intuitive way to interpret it is to assume that it always completes in a strict top-to-bottom order. Spawning threads introduces tremendous mental burden and becomes a constant source of different issues which are notoriously hard to debug.

It looks like “just spawn a new thread” solution is flawed so what are our options? Many traditional solutions rely on sending messages between the “worker” and UI threads, and they tend to complicate the code quite a bit. They also make it hard to handle different errors and edge cases.

If only there was a way to keep the code simple and avoid messing with the execution order. As you may have guessed, that’s exactly what coroutines are for. Let’s look at the following code:

suspend fun displayBooks() {
    progressBar.show()

    withContext(Dispatchers.IO) {
        loadBooks()
    }

    progressBar.hide()
}

It looks similar to the previous example but let’s look at the order of execution:

As you can see, the UI thread is not getting blocked while the data loads, but it also doesn’t proceed to the next line until the data is ready. This ability to wait without blocking the thread is called “suspension”. If you look at the function definition, you will see the suspend keyword was added before the fun keyboard. That’s how we let Kotlin know that we want a particular function to be able to suspend its own execution and to free its thread for other tasks, when necessary.

In other words, coroutine is just a function that can be partially executed. Since coroutines are not that different from normal functions, you can convert any function into a coroutine just by adding suspend before the fun keyword.

Benefits of Coroutines

One of the main benefits of coroutines is their readability, they look similar to regular functions, but they are much more powerful. Code readability tends to be the victim when it comes to concurrency because Java concurrency APIs are complex and error prone and the alternatives such as RxJava can also complicate the code and mess with its structure.

Another important fact about coroutines is that they are lightweight. You can spawn hundreds of thousands of coroutines on your laptop, and it won’t crash your system. Threads, on the contrary, are expensive, and you should keep a close look on how many threads you program uses.

Costs of Coroutines

Coroutines don’t change the fact that you should not use concurrency unless you really need it. Here are the biggest cons of using coroutines in your code:

  • Coroutines and Kotlin are different things. There are lots of good programmers who are perfectly comfortable with Kotlin but know nothing about coroutines. Coroutines require a programmer to grasp a few new concepts such as suspending functions, jobs, dispatchers and structured concurrency. Some advanced and more complicated use cases also require a good understanding of hot/cold flows, and many patterns that were built upon them. Let’s be clear: all of that is a cost, and that cost is pretty high.

  • Badly implemented coroutines can break your programs in many ways, some of them aren’t that obvious and might take a lot of time to detect. Avoid concurrency unless it’s absolutely necessary.

  • Coroutines are harder to debug and debugging the code that uses coroutines is different from debugging vanilla Kotlin. One more thing to learn and support so make sure you really need it.

Anatomy of Coroutines: Context

Coroutine context is just a set of CoroutineContext.Element instances. Every coroutine should have a context, and it’s usually created automatically when you launch a coroutine using one of the popular coroutine builders. Let’s take CoroutineScope.launch() as an example:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

There are 4 different keys values which can be stored in CoroutineContext:

  • CoroutineName - used to name a coroutine, mostly for debugging purposes
  • Job - a cancellable thing, can have a parent job as well as children jobs. Jobs are hierarchical, and the cancellation of children jobs does not lead to cancellation of their parent jobs
  • ContinuationInterceptor - CoroutineContext instance that intercepts coroutine continuations
  • CoroutineExceptionHandler - an optional element used to handle uncaught exceptions

Anatomy of Coroutines: Dispatchers

Coroutine dispatchers determine what threads their coroutines should use for their execution. Imagine the situation when you have to interact with some UI elements, and you really need to access them from the UI thread. You can create a special CoroutineDispatcher for UI interactions, but it’s usually provided by the coroutines library.

There are 4 predefined dispatchers that should be enough for most of the apps:

  • Default: That’s what coroutines use by default. This dispatcher has a pool of threads which is equal to the number of CPU cores available to your program so this dispatcher can run coroutines in parallel. One of the good uses for this dispatcher is to run any CPU intensive calculations. This way, we can split our coroutines evenly between all the CPU cores available.

  • IO: The main purpose of this dispatcher is to handle IO tasks such us reading from or writing into files or performing network requests. This dispatcher uses a pool of threads, so it’s also capable of parallel execution but this pool can be substantially bigger than the pool used by Default dispatcher.

  • Unconfined: This dispatcher is not confined to any thread which makes it unpredictable, and you should avoid using it unless you have a strong reason to do otherwise.

  • Main: The implementation of this dispatcher varies from platform to platform, and it is always confined to the UI thread. It’s probably a good idea to execute all of your UI interacting pieces of coroutines with this dispatcher.

Anatomy of Coroutines: Launchers

Here is the most popular ways to start a coroutine:

  • CoroutineScope.launch() - launches a coroutine in a non-blocking way
  • runBlocking() - launches a coroutine and block until it finishes. This function is useful in unit tests when you want to do all the assertions before the test function returns

Once started, your coroutine is free to call other coroutines directly.

Anatomy of Coroutines: Jobs

Each coroutine implements Job interface:

class Coroutine : Job

Job is a cancellable thing with a life-cycle that culminates in its completion. You can always check the state of your jobs by reading its isActive, isCompleted or isCancelled properties.

Probably the most important methods of Job interface are:

  • cancel() - cancels the job, and it’s children
  • join() - blocks until this job is complete

Anatomy of Coroutines: Async Builders and Deferred Results

There is a special coroutine builder called CoroutineScope.async() which lets you call another coroutine in a non-blocking way. This function gives you a Deferred result which means that it might not yet be available. You can just call the await() method at some point in the future, so it’s pretty easy to execute several coroutines in parallel and then just call the await() on them to get the results.

Anatomy of Coroutines: Scopes and Strucrured Concurrency

Lifecycle management is an important part of software development. Many apps are composed of many components and each of those components might have their own lifecycle which is separate from the rest of the app. Let’s say we have an Android app which has a special screen for displaying some kind of data that needs to be fetched from the server. A user might open this screen and see a loading indicator. At this point, he may keep waiting, or he might get bored and close the screen before the data loads. In case the user leaves, we need a way to wind down all the coroutines that are related to fetching that data but how can we do that? Well, that’s what scopes are for!

Every coroutine builder is an extension of CoroutineScope, so the only thing you have to do is to define a special scope for each component that has a separate lifecycle. Let’s take the Android’s ViewModel as an example:

class AuthViewModel(authRepository: AuthRepository) : ViewModel() {
    private val job = Job()
    private val uiScope = CoroutineScope(Dispatchers.Main + job)

    private val authResult = MutableLiveData<AuthResult>()

    override fun onCleared() = job.cancel()

    fun signIn(email: String, password: String) {
        uiScope.launch {
            val result = authRepository.signIn(email, password)
            authResult.value = result
        }
    }
}

As you can see, this component has its own scope which uses Dispatchers.Main as a default dispatcher. We can easily stop doing what we’re doing once we receive the onCleared() callback from the external component that manages ViewModels on Android.

Conclusion

Coroutines is a powerful addition to the Kotlin language, and they allow us to write complex multithreaded logic which doesn’t look very different from the code we usually write for single threaded use. Coroutines readability is superior to any of the popular alternatives, and I think that coroutines will become more popular among Kotlin developers in the future.