Zytkowski's Thought Dumpster

Kotlin Coroutines: made easy.

Recently I've been having a blast writing a CLI app in Rust to learn some core concepts about threading with the language, and while I'm not ready to write about it (far from it in fact) I thought I'd share some concepts and code snippets in Kotlin that made me have a better understanding of one the most brilliant features surrounding this language: coroutines.

Threads vs Coroutines

Before we get to the code, let's just take a moment to remind ourselves about how Coroutines differ (and don't) from Threads, and it's actually pretty simple.

Both Threads and Coroutines can be used to execute processes in parallel, but not necessarily, and they can also be used to just run things asynchronously. For example, Javalin's default configuration runs every request synchronously inside a thread, whilst Ktor embraces the language and runs every request inside a coroutine, but also synchronously.

Most importantly, coroutines run inside threads, and it's all managed by the language itself. The diagram below sums it up pretty neatly, but the main idea here is that whenever Kotlin spawns a coroutine for you, the code you write might run on a different thread, if the current one is busy with blocking code.

Zytkowski's Thought Dumpster

What is the point of using it?!

Simply put: coroutines are cheaper and smarter than threads. "Why is that so?", you might ask. Well, because coroutines are able to identify blocking code and lend the blocked time for another coroutine to run another job on the same thread. Think about it this way: if an HTTP request is executed in Thread-01, there is a moment during the execution where we are simply waiting for the server to return us the information we asked for, this can take any amount of time, and the thread is just standing there, doing nothing!.

Coroutines change that, because Kotlin is able to tell that we are running a blocking piece of code, thus, that thread can be used to compute some other coroutine while the other one is waiting for the server to respond. Pretty amazing, in my opinion.

How about when the thread is actually full of jobs and no other can run? Well, then Kotlin just spawns a new thread, and it's all good. 😎

The code you're looking for

There's a lot more to how coroutines work, you may refer to the official docs to learn more about it. For now, let's talk code. Here are the dependencies we're using:

// For the code itself
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// For testing
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

Everytime we need some piece of code to run inside a coroutine, we must use the suspend keyword ahead of the function definition, to tell the compiler that we need to be in a coroutine context to run that function.

Kotlin is pretty nice and allows us to use the suspend keyword in the main function as well, but if that wasn't the case, we'd be wrapping someWork() inside a runBlocking call, and that would create a coroutine context that would allow us to run suspend functions inside.

import kotlin.random.Random
import kotlinx.coroutines.delay

suspend fun main() {
    someWork()
}

suspend fun someWork() {
    delay(Random.nextLong(1_000, 10_000)) // Block the current coroutine
    // Notice that we use delay, instead of the classic Thread.sleep()
}

Based on the last couple of paragraphs, can you guess what would happen if we used Thread.sleep() instead of delay()?

Do you even throw, bruh?

Because every suspend call runs inside a coroutine that's created from the parent coroutine, if the code fails, exceptions are raised and subsequent calls are not reached, like in the example below.

suspend fun main() {
    someWork()
    // This line will never run because someWork throws an exception
    println("It Ain't Over 'Til It's Over")
}

suspend fun someWork() {
    delay(500)
    throw Exception("Test")
}

What if we want our coroutine code to contain exceptions so that the parent code runs unaffected? Then, our code would look something like the following:

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main() {
    CoroutineScope(Dispatchers.IO).launch {
        someWork()
    }
    println("It Ain't Over 'Til It's Over")
}

suspend fun someWork() {
    delay(500)
    throw Exception("Test")
}

In this snippet, we create a new CoroutineScope, isolated from the current context, based on the coroutine dispatcher which we frequently use for IO-based workloads. Dispatchers can be compared to thread pools, but that's not how one would precisely define them, you can read more about the specifics of the available dispatchers on the official docs, and also, remember that you can implement your own dispatcher, if you deem it necessary.

Summing it up: by isolating the running code in another scope, the exceptions are not raised to the parent scope, thus, they must be handled somewhere inside the .launch lambda.

In the future, we'll talk about why you should not explicitly define dispatchers inside the code, and instead take them as some kind of argument or dependency. That's a subject for another post, though.

Parallel workloads with coroutines

My personal opinion is that you should always write code that runs on coroutines, but if you don't want that, and just want to do some kind of parallel processing here and there, here's an example of how I'd do it:

import java.util.Base64
import kotlin.random.Random
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay

suspend fun main() {
    val executionTime = measureTimeMillis {
        encodeComposers()
    }
    println("Whole job took $executionTime ms")
}

suspend fun encodeComposers() {
    val composers = listOf(
        "Wolfgang Amadeus Mozart",
        "Ludwig van Beethoven",
        "Johann Sebastian Bach",
        "Pyotr Ilyich Tchaikovsky",
        "Franz Joseph Haydn",
        "Johannes Brahms",
        "Antonio Vivaldi",
        "George Frideric Handel",
        "Claude Debussy",
        "Richard Wagner",
    )

    // Creating 10 coroutine based jobs, that will run in parallel
    val composerEncodeJobs = composers.map {
        CoroutineScope(Dispatchers.IO).async {
            encryptComposerName(it)
        }
    }
    // Waiting for the execution of all deferred jobs
    val encodedComposers = composerEncodeJobs.awaitAll()
    // Doing whatever with the result
    encodedComposers.forEach {
        println(it)
    }
}

suspend fun encryptComposerName(composerName: String): String {
    delay(Random.nextLong(100, 500))
    println("Hello from thread ${Thread.currentThread().name}")
    return String(Base64.getEncoder().encode(composerName.toByteArray()))
}

Here's an example of the output:

Hello from thread DefaultDispatcher-worker-1
Hello from thread DefaultDispatcher-worker-2
Hello from thread DefaultDispatcher-worker-1
Hello from thread DefaultDispatcher-worker-1
Hello from thread DefaultDispatcher-worker-1
Hello from thread DefaultDispatcher-worker-2
Hello from thread DefaultDispatcher-worker-1
Hello from thread DefaultDispatcher-worker-1
Hello from thread DefaultDispatcher-worker-1
Hello from thread DefaultDispatcher-worker-1
V29sZmdhbmcgQW1hZGV1cyBNb3phcnQ=
THVkd2lnIHZhbiBCZWV0aG92ZW4=
Sm9oYW5uIFNlYmFzdGlhbiBCYWNo
UHlvdHIgSWx5aWNoIFRjaGFpa292c2t5
RnJhbnogSm9zZXBoIEhheWRu
Sm9oYW5uZXMgQnJhaG1z
QW50b25pbyBWaXZhbGRp
R2VvcmdlIEZyaWRlcmljIEhhbmRlbA==
Q2xhdWRlIERlYnVzc3k=
UmljaGFyZCBXYWduZXI=
Whole job took 567 ms

Notice that, in this run, two threads were used to run the 10 jobs, it might be less, or it might be more, that's why you must be aware of the relationship between threads and coroutines when inside a suspend function. Either way, we're not generating an unnecessary amount of threads, instead, they're being reused! Ain't that cool?

Conclusion

I hope that, if you had no prior knowledge of how coroutines work, you learned a little bit about them reading this, and also have some code snippets to toy with. And I'm excited to write more about some coroutine quirks in the future.

I have to disclose that I'm not a specialist in Kotlin or anything like that, I just love the language and I wrote this content with my own understandings of the feature. If you want a deep dive on the subject, some other fellas like Anton Arhipov and Manuel Vivo are definitely sources of knowledge on the subject.

I hate spanning content through multiple parts, but I also wanted to keep this a confortable read, so we will be talking about testing coroutine based features on another post in the near future.

#coroutines #kotlin #programming #threads