Clear and Concise Concurrency with Coroutines in Kotlin
Clear and Concise Concurrency with Coroutines in Kotlin
By Ruud Nimour
11 min read
Learn some Kotlin basics and how to easily use coroutines to write parallelized code cleanly according to structured concurrency.
- Authors
- Name
- Ruud Nimour
- linkedinRuud Nimour
- Github
- githubrnimour
Computers nowadays are blazingly fast. When they to do a really large amount of work though, it can still take a long time. If the work can be split up into smaller independent tasks which can be done in parallel however, we are in luck. We can leverage the fact that practically every computer has multiple cores to speed up this work by writing code that makes use of these extra cores. Writing such multithreaded code can be quite difficult, however, as it is easy to lose track of threads or run into race conditions.
Kotlin has introduced so-called "coroutines" to make this easier. In this article, we will shortly go over some of Java's multithreading possibilities, then explain some of Kotlin's syntax, and finally see how it's used to make it very easy to write clean concurrent code according to structured concurrency.
Java multithreading
Java has multiple ways to do multithreading. The most direct way is by simply creating and starting new Threads like so:
public static void main(String[] args) {
final int NUMBER_OF_THREADS = 10;
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
Thread thread = new Thread(() -> {
System.out.println("Hello from thread " + Thread.currentThread());
try {
Thread.sleep(5000); // simulate some work
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
Now, this works fine for moderate amounts of threads. On my machine however, cranking the number of threads to a modest ten thousand already crashes the program at thread number ~9200 with error 'unable to create native thread: possibly out of memory or process/resource limits reached', even though the threads aren't even doing anything! This is because Thread
s in Java are actually quite heavy (requiring several kilobytes of memory), and are bound to OS threads, which are limited in number.
Fortunately, Java's Project Loom introduces VirtualThread
s, which are now production-ready since Java 21. Rather than each created VirtualThread
also creating a new OS thread, instead, the JVM has several so-called "carrier threads" which actually run the virtual threads. This way, we can create many millions of virtual threads without problem. Because we use the same Thread
API, the only code change necessary is the new Thread(...
line above, which will become:
Thread thread = Thread.ofVirtual().unstarted(() -> {
With this simple code change, I can now create more than 1 million virtual threads this way. This is already big improvement, but as we will discuss later, in Kotlin we can do this much more nicely, and moreover forces you to code following the principle of structured concurrency, which I will explain later.
It should also be noted however, that Java provides other ways of doing multithreading. For example, rather than creating Thread
s directly, you can submit Runnable
tasks to an ExecutorService
with a limited number of threads, see for instance Executors.newFixedThreadPool(int nThreads)
. Another implementation of ExecutorService
is ForkJoinPool, which uses a "work stealing algorithm". If a thread in such a ForkJoinPool
is waiting for completion of its created subtasks, it will take work that other threads have in their work queue. This way, the work is distributed more evenly among the threads, and the threads are kept busy as much as possible.
Kotlin
Some Syntax
If you come from a pure Java background like me, it may be good to go over some Kotlin syntax. It will be heavily used in the following examples, and has some differences with Java. I won't be going over everything here though! Kotlin's own documentation is very good and I recommend you to check it out. If you're already familiar with Kotlin, you can skip over this part.
Variable definitions and String templates
In Kotlin, variables are declared using val
(immutable) or var
(mutable). Type declaration in Kotlin is optional if it can be inferred by the compiler. If we do specify the type, it comes with a colon after the variable name, like so: val name: String = "Ruud"
. Kotlin also has string interpolation, using a $
. For example: "my name is $name, and has ${name.length} letters"
. This way, you can easily insert variables (and any expressions!) into strings. As you can see, simple variable insertions do not need the {}
curly braces, but if we want to do method calls on them, we do.
Lambda functions
Another feature we will be using heavily, and the main reason I want to go over this syntax, is that the last (or as they call it, trailing) lambda function of a method can be placed outside the brackets. This was quite puzzling to me, but after using it, I found that it creates very readable code. For example, from the Kotlin standard library, we have the repeat
function:
public inline fun repeat(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}
This simply repeats the action times
times. (The Unit
return type is Kotlin's equivalent for returning void
)
For example, if you want to ask "are you sure" three times, you can do this:
repeat(3, {
println("are you sure?")
}) // why two sets of brackets here...
You can already see some subtle differences with Java's lambda functions. rather than (params) -> { body }
we have { params -> body }
. Note that in the case of having only a single parameter (like above), this parameter will be automatically available as it
, so as you can see we don't even need to have the params ->
part. The thing is that we can now apply the rule "last lambda can go outside the function call" to get:
repeat(3) {
println("are you sure?")
} // cleaner :)
to make it even simpler. In the case that there is only one parameter (the lambda) then the function call's brackets are not even necessary. For example, Iterable
's filter(predicate: (T) -> Boolean)
can be used to call simply
arrayListOf(-2, -1, 0, 1, 2)
.filter { it > 0 } // filter for only positive numbers: returns [1, 2]
No (round) brackets needed!
Extension functions and functions with receivers
The last feature I want to discuss here are functions with receivers. This ties in heavily with another feature of Kotlin, which are extension functions. These are functions on a class, but are defined outside of the class. For example:
// define a class with property "name"
class Person(var name: String)
fun Person.greet() {
// this method now has access to the "name" property
println("Hello, my name is $name")
}
val ruud = Person("Ruud")
ruud.greet() // prints "Hello, my name is Ruud"
As you can see, the greet
function is defined outside of the Person
class, but can be called on a Person
object. In other words, Person
is the receiver of the function. We can take this a step further by defining a function with a receiver. This is done by defining a lambda function with a receiver, like in this example:
fun greetRuud(init: Person.() -> Unit) {
val ruud = Person("Ruud")
ruud.init()
}
greetRuud { // instantiates Person("Ruud") which receives this lambda
greet()
println("I love playing games!")
}
// prints:
// Hello, my name is Ruud
// I love playing games!
As you can see, the greetRuud
function takes a lambda function. The notation was a bit puzzling to me at first, but it is essentially the same as the greet
extension function we defined on Person
, but now as a lambda parameter that I call init
. This also means that this extension function (or how Kotlin refers to it as a "function with receiver") has access to the Person
object's properties and functions, and as you can see, we were able to call the greet()
function.
Applying this to create DSL
Now before we finally dive into the juicy coroutines in Kotlin, let's see how the above concepts are applied to create a Domain Specific Language (DSL). It's not strictly speaking necessary to understand exactly how all of this works, but it is very interesting, and if you do understand, it allows you to create very powerful DSLs yourself.
A DSL is, according to jetbrains, "a programming language with a higher level of abstraction optimized for a specific class of problems." More abstraction means we can use a more human-readable language to solve a specific problem. To give a more concrete example (other than greeting me like earlier), we borrow an example from Kotlin Koans. This is a small crash course on Kotlin, which I can highly recommend to learn the basics of Kotlin, as you can immediately try out the concepts you have just learned. Near the end of the course, there is a section on DSLs, giving an HTML builder as an example.
When building an HTML page, you might want to create a table with table rows <tr>
and table data <td>
like
<table>
<tr>
<td>column header 1</td>
<td>column header 2</td>
</tr>
<tr>
<td>data row 1: abc</td>
<td>data row 1: def</td>
</tr>
</table>
In the Kotlin Koans example, we can very expressively create this table like so:
table {
tr {
td { text("column header 1") }
td { text("column header 2") }
}
tr {
td { text("data row 1: abc") }
td { text("data row 1: def") }
}
}
Effectively the same as the produced HTML. But since this is in Kotlin, we are free to use all of Kotlin's features available to us, like loops, if statements, etc. For example,
for (i in 1..10) {
tr {
td { text("data row $i: abc") }
td { text("data row $i: def") }
}
}
is a perfectly legal expression and produces ten <tr>
row tags with two <td>
data tags inside each.
So then the million-dollar question: how do we create this DSL, i.e. these table
, tr
, etc. functions? Again, please check out the Kotlin Koans task for all the details. I will only give a brief explanation here. The main data structure we need is a HTML Tag
, which may have a list of child Tag
s. We can define this as follows:
// define the Tag class (open so that we can subclass it)
open class Tag(val name: String) {
protected val children = mutableListOf<Tag>()
override fun toString() = "<$name>${children.joinToString("")}</$name>"
}
Then we can define the <table>
tag as a subclass of Tag
, which has a function tr
to add a <tr>
tag to its child list.
class TABLE : Tag("table") {
fun tr(init: TR.() -> Unit) {
val tr = TR() // create a new <tr> tag
tr.init() // add the "tr { ... }" block contents to the <tr> tag
children += tr // add the <tr> tag to (this) <table>'s "children" list
}
}
The TR
and TD
tags are defined similarly. For more information, check Kotlin's documentation on Type-safe builders.
Coroutines in Kotlin
Now we can finally dive into coroutines. As already stated, coroutines are a clean way to do multithreading in Kotlin. The main advantage of coroutines is that they are very lightweight, since they are not bound to OS threads, like Java's virtual threads. Another is that there is a coroutine DSL that effectively forces you to use "structured concurrency". What is this "structured concurrency"? According to Kotlin's own documentation, it means that coroutines (i.e. separate computations) can only be launched in a specific CoroutineScope
which delimits the lifetime of the coroutine. This means that a coroutine cannot complete until all its child coroutines complete. This keeps us from "losing" coroutines, which can happen in Java when a Thread
is created in a method, but the method returns before the Thread
completes. We will see in an example how this is enforced through the way we write coroutines.
Example: Merge Sort
In order to get a better understanding of coroutines, let's take a look at an example where we can easily use them. The merge sort algorithm is a good example, as it is a divide-and-conquer algorithm, which can be parallelized very well. The basic algorithm is as follows:
fun mergeSort(list: MutableList<Int>) {
if (list.size <= 1) {
return // already sorted
}
// split the list in two
val middle = list.size / 2
val left = list.subList(0, middle)
val right = list.subList(middle, list.size)
// sort both halves
mergeSort(left)
mergeSort(right)
// and merge the sorted lists back together
merge(list, left, right)
}
where the merge
function simply takes the lists (left
and right
) and merges them into a single sorted list. Because left
and right
are already sorted, merging them into a sorted list is very simple and fast. As you can see, this is a recursive algorithm, which calls itself twice. This is exactly where we can use coroutines to parallelize sorting the left and right halves of the lists, as these are independent tasks.
Adding coroutines
All we need to do is add the dependency "org.jetbrains.kotlinx:kotlinx-coroutines-core" to enable using coroutines in our Kotlin project, and then introduce coroutines to the mergeSort
function, like so:
suspend fun coMergeSort(list: MutableList<Int>) {
// ... same as before
coroutineScope {
launch { coMergeSort(left) }
launch { coMergeSort(right) }
}
merge(list, left, right)
}
I introduced the suspend
keyword to the function declaration so that we can use the coroutineScope
function. It indicates that this function's execution can be suspended, for example when waiting for its spawned coroutines to complete. The coroutineScope
function creates a new coroutine scope, in which we launch
two new coroutines (asynchronously) to sort the left and right halves of the list*. This means that the recursive calls to coMergeSort
to sort the left and right halves of the list are now done in parallel. Note that we only exit the coroutineScope
function when both launched coroutines complete, showing very clearly where coroutine execution ends, thus adhering to the principle of structured concurrency. This ensures that only after these two coroutines complete, the merge
function is called.
*= In terms of the nomenclature introduced in the previous section, the launch
function is a coroutine builder, available as an extension function on CoroutineScope
, which is the receiver of the coroutineScope
function.
When I try out our simple mergeSort and compare it to our shiny new coMergeSort sorting algorithm on a list of 4 and 16 million random integers, I found the following results:
Algorithm | 4 million integers | 16 million integers |
---|---|---|
mergeSort | 0.6 seconds | 1.8 seconds |
coMergeSort | 1.6 seconds | 6 seconds |
As we can see, wait, what!? Wasn't this supposed to go much faster using coroutines, since we utilize every core of our CPU? Yes, but although coroutines are lightweight, they are not no-weight. Creating a coroutine does carry some overhead, and since we create coroutines even for lists consisting of only one element... You can imagine that creating 16 million coroutines which do nothing but return their one-element list is not very efficient. Note that we still can do this without crashing the application though, unlike with Java's Thread
s, which already crashed at a mere ten thousand threads.
Let's not add the overhead of creating extra coroutines for small lists, and only create coroutines for lists larger than, say, 1000 items. This way, we can still parallelize the sorting of large lists, but not introduce the overhead for small lists. This is easily done by adding a check at the start of the function:
suspend fun smartCoMergeSort(list: MutableList<Int>) {
if (list.size <= 1000) {
mergeSort(list) // no need to use coroutines for small lists
return
}
// ... same as before
}
Testing the performance of this algorithm and comparing them with the previous results (and testing against even larger lists), we get:
Algorithm | 4 million integers | 16 million integers | 64 million integers |
---|---|---|---|
mergeSort | 0.6 seconds | 1.8 seconds | 7.5 seconds |
coMergeSort | 1.6 seconds | 6 seconds | 28 seconds |
smartCoMergeSort | 0.24 seconds | 0.55 seconds | 1.9 seconds |
Great success! We have increased the performance of our merge sort algorithm by a factor of about 3 to 4. It should be noted, however, that by incorrectly using them, we managed to actually reduce the performance by about the same factor.
In conclusion, here we have seen a good example of how coroutines can be very powerful, but also how we must still think about how we use them. I had a blast learning about coroutines in Kotlin, and this is only the start. For example, we can also use coroutines to handle non-blocking HTTP requests, also I never dived into coroutine exception handling, or timeouts etc.
You can check my GitHub "sorters" repository to see the code in action, or to play around with it yourself.
Upcoming events
Mastering Event-Driven Design
PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED Are you and your team struggling with event-driven microservices? Join us for a meetup with Mehmet Akif Tütüncü, a senior software engineer, who has given multiple great talks so far and Allard Buijze founder of CTO and founder of AxonIQ, who built the fundaments of the Axon Framework. RSVP for an evening of learning, delicious food, and the fusion of creativity and tech! 🚀 18:00 – 🚪 Doors open to the public 18:15 – 🍕 Let’s eat 19:00 – 📢 Getting Your Axe On Event Sourcing with Axon Framework 20:00 – 🍹 Small break 20:15 – 📢 Event-Driven Microservices - Beyond the Fairy Tale 21:00 – 🙋♀️ drinks 22:00 – 🍻 See you next time? Details: Getting Your Axe On - Event Sourcing with Axon Framework In this presentation, we will explore the basics of event-driven architecture using Axon Framework. We'll start by explaining key concepts such as Event Sourcing and Command Query Responsibility Segregation (CQRS), and how they can improve the scalability and maintainability of modern applications. You will learn what Axon Framework is, how it simplifies implementing these patterns, and see hands-on examples of setting up a project with Axon Framework and Spring Boot. Whether you are new to these concepts or looking to understand them more, this session will provide practical insights and tools to help you build resilient and efficient applications. Event-Driven Microservices - Beyond the Fairy Tale Our applications need to be faster, better, bigger, smarter, and more enjoyable to meet our demanding end-users needs. In recent years, the way we build, run, and operate our software has changed significantly. We use scalable platforms to deploy and manage our applications. Instead of big monolithic deployment applications, we now deploy small, functionally consistent components as microservices. Problem. Solved. Right? Unfortunately, for most of us, microservices, and especially their event-driven variants, do not deliver on the beautiful, fairy-tale-like promises that surround them.In this session, Allard will share a different take on microservices. We will see that not much has changed in how we build software, which is why so many “microservices projects” fail nowadays. What lessons can we learn from concepts like DDD, CQRS, and Event Sourcing to help manage the complexity of our systems? He will also show how message-driven communication allows us to focus on finding the boundaries of functionally cohesive components, which we can evolve into microservices should the need arise.
| Coven of Wisdom - Utrecht
Go to page for Mastering Event-Driven DesignThe Leadership Meetup
PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED What distinguishes a software developer from a software team lead? As a team leader, you are responsible for people, their performance, and motivation. Your output is the output of your team. Whether you are a front-end or back-end developer, or any other discipline that wants to grow into the role of a tech lead, RSVP for an evening of learning, delicious food, and the fusion of leadership and tech! 🚀 18:00 – 🚪 Doors open to the public 18:15 – 🍕 Let’s eat 19:00 – 📢 First round of Talks 19:45 – 🍹 Small break 20:00 – 📢 Second round of Talks 20:45 – 🙋♀️ drinks 21:00 – 🍻 See you next time? First Round of Talks: Pixel Perfect and Perfectly Insane: About That Time My Brain Just Switched Off Remy Parzinski, Design System Lead at Logius Learn from Remy how you can care for yourself because we all need to. Second Round of Talks: Becoming a LeadDev at your client; How to Fail at Large (or How to Do Slightly Better) Arno Koehler Engineering Manager @ iO What are the things that will help you become a lead engineer? Building Team Culture (Tales of trust and positivity) Michel Blankenstein Engineering Manager @ iO & Head of Technology @ Zorggenoot How do you create a culture at your company or team? RSVP now to secure your spot, and let's explore the fascinating world of design systems together!
| Coven of Wisdom - Amsterdam
Go to page for The Leadership MeetupCoven of Wisdom - Herentals - Spring `24 edition
Join us for an exciting web technology meetup where you’ll get a chance to gain valuable insights and knowledge about the latest trends in the field. Don’t miss out on this opportunity to expand your knowledge, network with fellow developers, and discover new and exciting possibilities. And the best part? Food and drinks are on us! Johan Vervloet - Event sourced wiezen; an introduction to Event Sourcing and CQRS Join me on a journey into the world of CQRS and Event Sourcing! Together we will unravel the misteries behind these powerful concepts, by exploring a real-life application: a score app for the 'Wiezen' card game.Using examples straight from the card table, we will delve into the depths of event sourcing and CQRS, comparing them to more traditional approaches that rely on an ORM.We will uncover the signs in your own database that indicate where event sourcing can bring added value. I will also provide you with some tips and pointers, should you decide to embark on your own event sourcing adventure. Filip Van Reeth - WordPress API; "Are you talking to me?" What if the WordPress API could be one of your best friends? What kind of light-hearted or profound requests would it share with you? In this talk, I would like to introduce you to it and ensure that you become best friends so that together you can have many more pleasant conversations (calls). Wanna be friends? Please note that the event or talks will be conducted in Dutch. Want to give a talk? Send us your proposal at meetup.herentals@iodigital.com 18:00 - 19:00: Food/Drinks/Networking 19:00 - 21:00: Talks 21:00 - 22:00: Networking Thursday 30th of May, 18h00 - 22h00 CET iO Campus Herentals, Zavelheide 15, Herentals
| Coven of Wisdom Herentals
Go to page for Coven of Wisdom - Herentals - Spring `24 edition