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
Drupal CMS Launch Party
Zoals sommigen misschien weten wordt op 15 Januari een nieuwe distributie van Drupal gelanceerd. Namelijk Drupal CMS (ook wel bekend als Starshot). Om dit te vieren gaan we op onze campus een klein eventje organiseren. We gaan die dag samen de livestream volgen waarbij het product gelanceerd wordt. De agenda is als volgt: 17u – 18u30: Drupal CMS livestream met taart 18u30 – 19u00: Versteld staan van de functionaliteiten 19u – 20u: Pizza eten en verder versteld staan van de functionaliteiten Laat ons zeker weten of je komt of niet door de invite te accepteren! Tot dan!
| Coven of Wisdom Herentals
Go to page for Drupal CMS Launch PartyCoven of Wisdom - Herentals - Winter `24 edition
Worstelen jij en je team met het bouwen van schaalbare digitale ecosystemen of zit je vast in een props hell met React of in een ander framework? Kom naar onze meetup waar ervaren sprekers hun inzichten en ervaringen delen over het bouwen van robuuste en flexibele applicaties. Schrijf je in voor een avond vol kennis, heerlijk eten en een mix van creativiteit en technologie! 🚀 18:00 – 🚪 Deuren open 18:15 – 🍕 Food & drinks 19:00 – 📢 Building a Mature Digital Ecosystem - Maarten Heip 20:00 – 🍹 Kleine pauze 20:15 – 📢 Compound Components: A Better Way to Build React Components - Sead Memic 21:00 – 🙋♀️ Drinks 22:00 – 🍻 Tot de volgende keer? Tijdens deze meetup gaan we dieper in op het bouwen van digitale ecosystemen en het creëren van herbruikbare React componenten. Maarten deelt zijn expertise over het ontwikkelen van een volwassen digitale infrastructuur, terwijl Sead je laat zien hoe je 'From Props Hell to Component Heaven' kunt gaan door het gebruik van Compound Components. Ze delen praktische inzichten die je direct kunt toepassen in je eigen projecten. 📍 Waar? Je vindt ons bij iO Herentals - Zavelheide 15, Herentals. Volg bij aankomst de borden 'meetup' vanaf de receptie. 🎫 Schrijf je in! De plaatsen zijn beperkt, dus RSVP is noodzakelijk. Dit helpt ons ook om de juiste hoeveelheid eten en drinken te voorzien - we willen natuurlijk niet dat iemand met een lege maag naar huis gaat! 😋 Over iO Wij zijn iO: een groeiend team van experts die end-to-end-diensten aanbieden voor communicatie en digitale transformatie. We denken groot en werken lokaal. Aan strategie, creatie, content, marketing en technologie. In nauwe samenwerking met onze klanten om hun merken te versterken, hun digitale systemen te verbeteren en hun toekomstbestendige groei veilig te stellen. We helpen klanten niet alleen hun zakelijke doelen te bereiken. Samen verkennen en benutten we de eindeloze mogelijkheden die markten in constante verandering bieden. De springplank voor die visie is talent. Onze campus is onze broedplaats voor innovatie, die een omgeving creëert die talent de ruimte en stimulans geeft die het nodig heeft om te ontkiemen, te ontwikkelen en te floreren. Want werken aan de infinite opportunities van morgen, dat doen we vandaag.
| Coven of Wisdom Herentals
Go to page for Coven of Wisdom - Herentals - Winter `24 editionThe Test Automation Meetup
PLEASE RSVP SO THAT WE KNOW HOW MUCH FOOD WE WILL NEED Test automation is a cornerstone of effective software development. It's about creating robust, predictable test suites that enhance quality and reliability. By diving into automation, you're architecting systems that ensure consistency and catch issues early. This expertise not only improves the development process but also broadens your skillset, making you a more versatile team member. Whether you're a developer looking to enhance your testing skills or a QA professional aiming to dive deeper into automation, RSVP for an evening of learning, delicious food, and the fusion of coding and quality assurance! 🚀🚀 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: The Power of Cross-browser Component Testing - Clarke Verdel, SR. Front-end Developer at iO How can you use Component Testing to ensure consistency cross-browser? Overcoming challenges in Visual Regression Testing - Sander van Surksum, Pagespeed | Web Performance Consultant and Sannie Kwakman, Freelance Full-stack Developer How can you overcome the challenges when setting up Visual Regression Testing? Second Round of Talks: Omg who wrote this **** code!? - Erwin Heitzman, SR. Test Automation Engineer at Rabobank How can tests help you and your team? Beyond the Unit Test - Christian Würthner, SR. Android Developer at iO How can you do advanced automated testing for, for instance, biometrics? RSVP now to secure your spot, and let's explore the fascinating world of test automation together!
| Coven of Wisdom - Amsterdam
Go to page for The Test Automation Meetup