スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Introduction to Kotlin Coroutines

Overview

Hi! I am @padobariya working as a mobile engineer with Quipper (Japan office).

In this post, I will talk about basics of Kotlin coroutines, as many of you may already know Kotlin coroutines are no longer experimental as of Kotlin 1.3. It is one of most promising feature for writing asynchronous, non-blocking code (and much more) which can help you to get rid of callbacks hell in your code base. We will go through how to start and some basics of using Kotlin coroutines with the help of the kotlinx.coroutines library, which is a collection of helpers and wrappers for existing Java libraries.

Summary

I guess one of the most challenging things in software development is anything that is asynchronous. Over the time we’ve seen many asynchronous APIs and libraries and Kotlin Coroutines is the latest addition to the toolbox.

One can think of a coroutine as a light-weight thread. Like threads, coroutines can run in parallel, wait for each other and communicate. The biggest difference is that coroutines are very lightweight, almost free: we can create thousands of them, and pay very little in terms of performance.

For me, the best part about coroutines is the structure of the code doesn’t change if you compare it with something synchronous. You don't need to learn any new programming paradigms in order to use coroutines.

Let's get started.

Dependencies

As you may have already know coroutine is not a part of Kotlin core API, we need to add following libraries in order to use Kotlin coroutines (you can also add other modules that you need)

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0-RC1'
}

Note: I’ve added kotlinx-coroutines-android dependency which is android specific, you don’t need to add if you are not developing for Android. Basically, it provides Dispatchers.Main to run coroutines on main/UI thread.

Launching a coroutine

You can start new coroutine with the help of launch or async function. Conceptually, async is just like launch difference is as follows. Note: async and await are not keywords in Kotlin and are not even part of its standard library.

Launch

Launches new coroutine without blocking current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is canceled. You can call join on this Job to block until this launch thread completes. Let’s launch our first coroutine using launch

fun foo() {
    GlobalScope.launch { // launch new coroutine in background
        delay(1000L) // non-blocking delay for 1 second        
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

This will generates following output.

Hello,
World!

Here we used non-blocking delay(...) and blocking Thread.sleep(...) bit confusing right? We can define this more clear way using runBlocking {...}

fun foo() {
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    }
}

The result is the same, but this code uses only non-blocking delay. The main thread, that invokes runBlocking, blocks until the coroutine inside runBlocking completes.

We can write above in more idiomatic way by wrapping foo() in runBlocking{...}

fun foo() = runBlocing<Unit> {
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive 
}

Here runBlocking { ... } works as an adaptor that is used to start the top-level main coroutine. We explicitly specify its Unit return type, because a well-formed main function in Kotlin has to return Unit.

Now, as you may have noticed delaying for a time while another coroutine is working is not a good approach. So let's explicitly wait (in a non-blocking way) until the background Job that we have launched is complete:

//With Join
fun foo() = runBlocking {
    // launch new coroutine and keep a reference to its Job
    val job = GlobalScope.launch {         
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes
}

Now the result is still the same, but the code of the main coroutine is not tied to the duration of the background job in any way. Much better.

Async

Conceptually, async is just like launch. It starts a separate coroutine which is a light-weight thread that works concurrently with all the other coroutines. The difference is that launch returns a Job and does not carry any resulting value, while async returns a Deferred -- a light-weight non-blocking future that represents a promise to provide a result later. Basically, async starts a background thread, does something, and returns a token immediately as Deferred.

fun foo() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingOne() }
        val two = async { doSomethingTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

You can use .await() on a deferred value to get its eventual result, but Deferred is also a Job, so you can cancel it if needed.

Dispatchers and threads

When we launch coroutine we need to specify dispatchers which determines what thread or threads the corresponding coroutine uses for its execution. If you are an android developer you will use the following departures:

  • Dispatchers.Main : Dispatch execution onto the Android main UI thread (Android Specific)
  • Dispatchers.IO : Dispatch execution in the background thread

Coroutine scope

As you may have noticed earlier we have used GlobalScope to launch coroutine. Basically, you need to provide scope to each coroutine it can be CoroutineScope or GlobalScope GlobalScope: As the name suggests the lifetime of the new coroutine is limited only by the lifetime of the whole application. CoroutineScope: This is lifecycle aware scope. This should be implemented on entities with a well-defined lifecycle that are responsible for launching children coroutines.

//Global scope
Class MainFragment : Fragment() {
   Private fun loadData() = GlobalScope.launch {} 
}

//Coroutine scope
class MainFragment : Fragment(), CoroutineScope {
   override val coroutineContext: CoroutineContext
      get() = Dispatchers.Main
   private fun loadData() = launch {} 
}

Coroutine using withContext

withContext is used to switch coroutine context. In following snippet loadData() will be executed in background/IO thread.

private fun loadData() = GlobalScope.launch(Dispatches.Main) {
   view.showLoading() // main thread

   //background thread
   val result = withContext(Dispatchers.IO) {provider.loadData()}

   view.showData(result) // main thread
}

The parent coroutine is launched via the launch function with the Main dispatcher. The background job is executed via withContext function with the IO dispatcher.

Multiple tasks sequentially In following case, result1 and result2 are executed sequentially

private fun loadData() = GlobalScope.launch(Dispatches.Main) {
   view.showLoading() // main thread

   //background thread
   val result1 = withContext(Dispatchers.IO) {provider.loadData1()}

   //background thread
   val result2 = withContext(Dispatchers.IO) {provider.loadData2()}

   val result = result1 + result2

   view.showData(result) // main thread
}

Multiple tasks parallel

In following case result1 and result2 are executed in parallel

private fun loadData() = GlobalScope.launch(Dispatches.Main) {
   view.showLoading() // main thread

   //background thread
   val result1 = async(Dispatchers.IO) {provider.loadData1()}

   //background thread
   val result2 = async(Dispatchers.IO) {provider.loadData2()}

   val result = result1 + result2

   view.showData(result) // main thread
}

Timeout

We can also specify timeout for a coroutine job.

private fun loadData() = GlobalScope.launch(Dispatches.Main) {
   view.showLoading() // main thread

   val task = async(Dispatchers.IO) {provider.loadData()}

   //background thread
   val result = withTimeoutOrNull(5, TimeUnit.SECOND) {task.await()}

   view.showData(result) // main thread
}

Exception Handling

So far we concluded that coroutines are awesome! but sometimes the world is not as awesome as we think and we have to deal with it. In coroutines same as synchronous programming you can use try catch to handle exceptions.

As the post is already getting long we can not cover all aspects of exception handling here. I strongly suggest viewing KotlinConf 2018 video to know how exception handling works in complex coroutines system.

private fun loadData() = GlobalScope.launch(Dispatches.Main) {
   view.showLoading() // main thread

   try {
      //background thread
      val result = async(Dispatchers.IO) {provider.loadData()}
      view.showData(result) // main thread
   } catch(ex: Exception) {
      ex.printStackTrace()
   }
}

To avoid try catch in parent class you can catch exception in loadData() and return generic result.

To Be Continued …

Thank you for giving this article a read. I hope you found this article interesting. In the next article, we will dive into more details about coroutines performance and how we can replace RxJava with coroutines.

Ohh by the way, here are some helpful links to help you with understanding coroutines