Kotlin Coroutines

Coroutine

Kotlin introduced coroutines starting with version 1.1.

As a language, Kotlin provides only the minimum low-level APIs in the standard library so that coroutine libraries can be used in many different forms. In Kotlin, async and await are not keywords and are not part of the standard library. Also, Kotlin’s concept of suspend functions provides an abstraction for asynchronous processing that is safer and less error-prone than futures and promises.

Coroutines are lightweight threads

Coroutines can be considered lightweight threads. They are not threads, but they make asynchronous programming possible.
Like threads, they can run processing in parallel without blocking other processing. However, they are very lightweight.
Threads have non-negligible startup and termination costs, but coroutines cost so little that you usually do not need to worry about them.
You can easily run thousands or tens of thousands of coroutines at the same time.

Coroutines is short for Co + Routines. Co means cooperation, and routines means functions. It means functions that cooperate with each other, and this is called a coroutine.

Gradle dependency setup

To use coroutines, configure the following dependency.

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
}

Additional dependencies can be configured at https://github.com/Kotlin/kotlinx.coroutines.

Coroutine basics

To understand coroutines, you need to understand the concept of suspend. In coroutines, processing is suspended instead of blocking a thread. Let’s look at the difference between block and suspend.

block
Blocking occupies a thread. When a thread is occupied, that thread cannot continue other processing.

fun main(){
    println("Coroutine")
    Thread.sleep(3000L) // Processing stops for 3 seconds. During this time, no processing proceeds at all.
    println("Hello")
}

Output:

Coroutine
Hello

suspend
Suspend pauses coroutine processing and releases the thread. While the thread is released, resources can be used for other processing.

import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    launch {
        delay(3000L) // Suspends processing for 3 seconds. Because the thread is released, Hello is displayed first.
        println("Coroutine")
    }
    println("Hello")
}

Output:

Hello
Coroutine

With block, the thread is occupied for 3 seconds. For example, if a thread is occupied while an application is being used, a freeze can occur and the user may not be able to operate the screen.
With suspend, on the other hand, only the processing is suspended, so the user can continue operating for those 3 seconds.

To use coroutines, you need to properly understand suspend.

First coroutine

A coroutine is an instance of a suspendable computation. It is conceptually similar to a thread in the sense that it runs a code block that works concurrently with other code. However, a coroutine is not bound to a specific thread. It can suspend execution on one thread and resume on another.

You can think of a coroutine as a lightweight thread, but actual usage differs from threads in several major ways.

fun main() = runBlocking {  // this: coroutineScope
    launch { // starts a new coroutine
        delay(1000L) // non-blocking delay for 1 second
        println("World!") // displayed after 1 second
    }
    println("Hello") // the main thread continues while the preceding thread is delayed.
}

Output:

Hello
World!

Now let’s analyze what this code does.

  • launch is a coroutine builder. It starts a new coroutine concurrently with the rest of the code and continues to run independently. Therefore, Hello is displayed first.
  • delay is a special suspend function. It suspends the coroutine for a specific amount of time and releases the main thread. The coroutine with delay is suspended, but the main thread is not blocked.
  • runBlocking is also a coroutine that bridges the non-coroutine world of fun main() and the coroutines inside the runBlocking {...} block. For example, launch is declared only in a coroutineScope.

The name runBlocking means that the thread running it, in this case the main thread, is blocked during the call until all coroutines inside runBlocking {...} finish execution.

Blocking an expensive resource such as a thread is inefficient and undesirable, so runBlocking is rarely used in real application code.

Structured concurrency

Coroutines follow the principle of structured concurrency. That is, a new coroutine can only be started in a specific coroutineScope that defines the coroutine’s lifetime. In real applications, many coroutines are started, but structured concurrency prevents them from being lost or missed.

Extract function refactoring

Let’s extract the code block inside launch {...} into another function. When extracting code from a coroutineScope, use the suspend modifier.

import kotlinx.coroutines.*

fun main() = runBlocking {  // this: coroutineScope
    launch { doWorld() }
    println("Hello")
}

suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

Output:

Hello
World!

Scope builder

In addition to coroutine scopes provided by various builders, you can declare your own scope with the coroutineScope builder. When a coroutine scope is created, it does not complete until all launched processing completes.

The runBlocking builder and the coroutineScope builder look similar because both wait for their own processing and all child thread processing to finish. The main differences are as follows.

  • runBlocking blocks the current thread until child threads complete.
  • coroutineScope does not block the current thread while waiting for child threads to complete.

Because of this difference, runBlocking is a regular function, and coroutineScope runs as a suspend function.

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: coroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

Output:

Hello
World!
fun main() = runBlocking { // serial
    countToDown()
    println("fire!")
}

suspend fun countToDown() = coroutineScope { // concurrent
    launch {
        delay(2000L)
        println("1")
    }
    launch {
        delay(1000L)
        println("2")
    }
    println("3")
}

Output:

3
2
1
fire!

An explicit job

The launch coroutine builder returns a Job object, which is a handle to the started coroutine and can be used to explicitly wait for completion. For example, the example below waits until the child coroutine completes, and then the string “Done” is printed.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch { // starts a new coroutine and keeps a reference to the job.
        delay(1000L)
        println("World!")
    }
    println("Hello")
    job.join() // waits until the child coroutine completes.
    println("Done")
}

Output:

Hello
World!
Done

Coroutines are lightweight

Start 100K coroutines, and after 5 seconds each coroutine prints a dot (.).

Execution with lightweight coroutines succeeds, but execution with threads causes java.lang.OutOfMemoryError.

import kotlinx.coroutines.*

fun main() {
    launchCoroutines() // success!
    runThreads() // java.lang.OutOfMemoryError
}

private fun launchCoroutines() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

private fun runThreads() {
    repeat(100_000) { // launch a lot of threads
        object : Thread() {
            override fun run() {
                sleep(5000L)
                print(".")
            }
        }.start()
    }
}

Summary

Here are the important points from what we learned this time.

  • Coroutines are a lightweight and safe way to handle asynchronous processing.
  • They use suspend instead of blocking threads.
  • They are not bound to a specific thread, so they can resume on another thread.
  • A coroutine started inside coroutineScope does not complete until all child processing has completed.

References