arrow left
Back to Developer Education

Introduction to Kotlin Coroutines

Introduction to Kotlin Coroutines

Coroutines were introduced in Kotlin 1.1. They brought about a new way of writing asynchronous, non-blocking code. An asynchronous code (from asynchronous programming) is code that runs parallel to others. It is also called non-blocking since it does not block the main thread. Asynchronous programming helps in running multiple unrelated tasks faster. In synchronous programming, the code is executed line by line. <!--more-->

Introduction

This means that in a program of five statements, the fifth statement is only executed after all the other statements are done. However, asynchronous programs run each statement/function parallel to each other. This article goes through some of the basics of writing asynchronous code using coroutines.

Prerequisites

To follow through this tutorial, you will need to:

  • Have IntelliJ installed.
  • Have a basic understanding of the Kotlin programming language.

Step 1 — Creating a Kotlin project

In this step, we are going to create a console kotlin project that is managed by gradle.

Open IntelliJ and select new project.

On the next window, select kotlin, console application.

Choose the project JDK, download one if none is installed.

New project

Give the project a name and click next. Leave the next screen to default settings and click finish.

Wait for the project build to finish.

Open the build.gradle file and add the following dependency.

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")

We are all set. Let's now create our first coroutine.

Step 2 — Creating our first coroutine

A coroutine is like a lightweight thread. All coroutines run on a pool of threads. Coroutines help us sequentially write asynchronous code.

Write the following code inside the main function.

GlobalScope.launch {
        delay(2000)
        print("World")
    }
println("Hello ")
Thread.sleep(3000)

Upon running the main function, World appears two seconds after Hello.

We use the launch coroutine builder to launch a coroutine. The coroutine needs to be launched in a scope.

A scope is used to control the lifecycle of a coroutine. Here, we have used the GlobalScope, that means that the coroutine will be limited to the lifecycle of the application.

The delay function is a suspending function. We will talk about suspending functions later in the article. The delay function pauses the coroutine rather than the thread. Pausing a thread is performance-costly since all tasks in the thread will be paused.

We have used the Thread.sleep function to wait for the coroutine to finish. If you leave it out. The main function will finish execution before the coroutine does, thus the output will be Hello. However, since we have to avoid sleeping the thread, we can use another coroutine builder known as runBlocking. The runBlocking coroutine builder blocks the thread until the coroutine has finished executing.

Replace Thread.sleep with:

runBlocking { delay(3000) }

This does the same task but using delay rather than Thread.sleep.

Step 3 — Joining and canceling coroutines

In the previous step, we saw how to launch a coroutine. However, the code above fires and forgets. It just lets the coroutine run. We can neither wait for it nor cancel it.

We used delay to force the main thread to wait for the coroutine to finish. This works for the example but cannot be applied in real-world applications. This is because we cannot determine how long the coroutine will take to finish execution.

Joining a coroutine can be seen as waiting for it. A coroutine builder returns a Job object. It is on this object that we can use the join function to wait on the coroutine. Let's try that out.

Go back to the main function. Store the coroutine job in a variable.

val job: Job = GlobalScope.launch {
    delay(2000)
    print("World")
}

Now use the variable to call the join function below it.

job.join()

Oops! we get an error after adding that function. This is because the join function is a suspending function, and suspending functions can only be called in a coroutine or other suspending functions.

To solve this error, wrap the whole code in a runBlocking coroutine builder as shown.

runBlocking {
    val job = this.launch {
        delay(2000)
        print("World")
    }
    println("Hello ")
    job.join()
}

Now run the program. The program runs as expected; Hello appears first then World appears after two seconds.

Coroutines are automatically canceled when they finish their 'job'. However, we might want to cancel an ongoing coroutine due to certain reasons, such as the coroutine running for a long time. The Job object gives us the cancel function. As the name suggests, it cancels the coroutine. Let's see it in practice.

Replace the main function with the following code.

runBlocking {
    val job = this.launch {
        delay(10000)
        print("World")
    }
    println("Hello ")
    delay(5000)
    job.cancel()
    job.join()
}

The program above prints Hello and stops after five seconds. We use delay to simulate a long-running task. The coroutine that should display World delays for ten seconds whereas the main coroutine delays for five seconds. This indicates that the function expects the coroutine to finish in five seconds, which is not the case.

Therefore, we cancel the coroutine since it has taken more time than expected. Notice that we have also used the join function. This waits for the coroutine to finish canceling. Since these two methods are used together most of the time, a function that combines these two steps was created. It is called cancellAndJoin. It does the same task but with less code.

Step 4 — Returning values from a coroutine

Not all coroutines are fire and forget, we sometimes need to return a value from a coroutine. To do this, we use the async coroutine builder. This builder returns a Deferred object of the type returned by the coroutine. We use the await function of the deferred object to get the result. Let's look at an example.

Copy the following code in the main function.

runBlocking {
    val job1 = this.async {
        delay(2000)
        500
    }

    val job2 = this.async {
        delay(2000)
        700
    }
    print(job1.await() + job2.await())
}

In the code above, we create two jobs. These jobs return deferred integer values. We use the delay function to simulate a heavy operation. By using the await function, the program waits for the coroutines to return their results before doing the addition.

Step 5 — Suspending functions

Through the previous steps, we have seen how to start and manage coroutines. However, how can we extract the workload of a specific coroutine to a function? Well, that's very simple.

We use the suspend keyword to declare a function as a coroutine. Marking a function as a suspending function gives it the ability to call other suspending functions like delay. This also restricts them to be called inside coroutines or other suspending functions only.

The alternative to wrapping our main code in a runBlocking builder, is to mark it as suspending. This will allow it to call other suspending functions.

This is how our main functions will look like.

suspend fun main() {
    val job1 = GlobalScope.async {
        delay(2000)
        500
    }

    val job2 = GlobalScope.async {
        delay(2000)
        700
    }
    print(job1.await() + job2.await())
}

It will still give the same result.

Concurrency test

Let's test whether the coroutines run in parallel.

Write the following code in the main function.

suspend fun main() {
    val timeTaken = measureTimeMillis {
        val jobs = (1..1000).map {
            GlobalScope.launch {
                delay(2000)
            }
        }
        jobs.joinAll()
    }
    println(timeTaken)
}

Here, we create an array of one thousand coroutines. Each coroutine delays for two seconds. We have used the measureTimeMillis method to measure the time taken to complete the tasks. If the coroutines don't run in parallel, our function will take two thousand seconds to complete. However, the function takes about two seconds to run on my machine. This clearly shows that these coroutines do run in parallel.

Conclusion

This article has gone through the basics of writing asynchronous code using coroutines. Coroutines help us consecutively write asynchronous code. We don't use callbacks as in other libraries. We have seen how coroutines are launched, joined to, and canceled.

We have also seen how we can obtain values from coroutines and how to create suspending functions. This artice is meant to be a foundation for coroutines. You can discover other features from their documentation. I hope you find this useful.

Happy coding!


Peer Review Contributions by: Linus Muema

Published on: Jan 25, 2021
Updated on: Jul 12, 2024
CTA

Start your journey with Cloudzilla

With Cloudzilla, apps freely roam across a global cloud with unbeatable simplicity and cost efficiency