Asynchronous processing is one of the must-have features every single programming language has. Whether it is built-in or enhanced by various external libraries, async capabilities come in many formats, styles, and rules.
When it comes to the Java Virtual Machine (JVM) world, Java threads reigned absolute for many years. When Kotlin came around, it decided to simplify the process (as it did for many other JVM features) of managing async code via coroutines.
If you have ever worked with JavaScript, for example, you may be familiar with the most famous keywords reserved for that purpose: async and await – the former used for determining the asynchronous code block and the latter for awaiting its completion.
In Kotlin, these keywords are not part of the standard library, which is why developers are required to import the kotlinx.coroutines library to make use of them. Apart from that, Kotlin also provides other keywords such as launch, coroutineScope, runBlocking, and more.
In this article, we are going to take the first steps towards understanding how Kotlin deals with async programming via coroutines. Let’s get into it!
Read: Introduction to Kotlin
What is a Kotlin Coroutine?
The best way to start understanding something is by playing around with it. So, let’s get straight into an example. Observe the Kotlin code below:
fun main() = runBlocking { doWorld() } suspend fun doWorld() = coroutineScope { // this: CoroutineScope launch { delay(1000L) println("World!") } println("Hello") }
This code snippet is extracted from the official Kotlin docs page. Here, you can see the usual main() function, where the execution starts, followed by a runBlocking block of code.
Simply put, a coroutine is a child of a suspendable function. It is very similar to working with threads in other languages, which means that the execution is performed concurrently with the rest of the non-async code.
That being said, a very important thing to know about Kotlin coroutines is that they are not tied to any specific thread, which means that a coroutine could start the execution from a particular thread, suspend it, and resume right after in another completely different thread, for example.
In the above example, we have some key actors, such as:
- runBlocking: This is a coroutine builder that draws a line separating the synchronous world from the asynchronous code running within the code block. In other words, whenever you want something to run asynchronously, this builder will provide that functionality within it.
- coroutineScope: This is very similar to runBlocking because it also waits for its body (and children) to complete. However, the difference is that it does not block the code execution like runBlocking does and, instead, it suspends the thread, making it available for other synchronous processing it may need to perform.
Another peculiarity of this function is that, when called from other external functions, it always requires that function to be marked as suspendable via the suspend operator.
- launch: This is another coroutine builder that tells the rest of the code to keep executing since the launch’s body will run in parallel.
- delay: This is also a suspending function, very similar to Thread.sleep from Java, which means that it pauses the execution for the given amount of time passed to it. However, note that this is only possible within async scopes, meaning that the rest of the synchronous execution will run normally.
As a result of the execution above, we have printed “Hello” in the first line, followed by “World!” – proving that the sync execution always continues no matter how much async processing you get in the code.
Read: Top 5 Online IDEs and Code Editors
Working with Jobs in Kotlin
A background job is a cancellable bit of code with a lifecycle that ends in its completion. Jobs can have state throughout their life cycles: children jobs that can be canceled recursively, failure control, and other capabilities.
Every time you call a launch builder, you are returning a Job that can be used to wait for the coroutine completion or canceled if need be.
Let’s take a look at an example job in Kotlin:
val job = launch { // launch a new coroutine and keep a reference to its Job delay(1000L) println("World!") } println("Hello") job.join() // wait until child coroutine completes println("Done")
As you can see, the object reference can be used later to wait for the inner coroutines to complete via the join() function. That is a very simple function if you are willing to give this control to other parts of the code.
The same could be applied to when you want to cancel it:
job.cancel()
Timeouts in Kotlin
There is another great feature that comes with coroutines related to timeouts. When running asynchronous operations, it is very important to be aware of flows that might run indefinitely, since they can eat up your available memory very quickly and lead to widespread errors in your application.
Every Java developer should be aware of the infamous OutOfMemoryError – much caution is taken to avoid it. If you are unfamiliar with it, it is an error that occurs in Java programs when the JVM is no longer able to allocate a specific object because it lacks space in the Java heap or when there is not enough native memory to support the loading of a Java class.
Kotlinx provides an easy way to deal with this issue, courtesy of the withTimeout() function, illustrated below:
withTimeout(2000L) { repeat(100) { delay(500L) } }
Whenever the operation exceeds the limit of milliseconds established, a CancellationException error will be thrown.
Another handy function to avoid these types of errors is withTimeoutOrNull, which suspends the execution for the specified timeout and, in case it is exceeded, the function returns null to the caller.
The “async” Keyword in Kotlin
As you might suspect, Kotlin also provides an async keyword for asynchronous processing. The async function creates coroutines and returns the “future” result as a Deferred, which is a Job with a result.
It does sound like it is very similar to launch, doesn’t it? However, it works by creating a completely separate lightweight thread-based coroutine that runs in parallel with all the other ones.
Other than this, launch functions are voided, which means that we can not extract future results from them, as opposed to async functions, which can. And, since they are also jobs, developers can cancel them later if you need to.
Observe the example below, showing how to use async in Kotlin:
runBlocking { val one = async { "One" } val two = async { "Two" } println("Results: ${one.await()}, ${two.await()}") }
Both async code blocks execute in parallel and you need to explicitly call the await() function if you want to access its results once they are ready (which, again, depends on how much processing time is contained within your async blocks).
It is worth noting that these blocks of code must also exist within suspendable functions if you want to spread them away from the runBlocking.
Read: Best Practices for Asynchronous Programming in Java
Conclusion to Kotlin Coroutines
There is much more complexity you can learn with Kotlin Coroutines that we would not be able to easily address here, so we will leave it to the official docs and future Kotlin tutorials to fill in the blanks.
Remember that, apart from the raw usage of coroutines inside of your Kotlin projects, you can also integrate it with other platforms, such as Spring Boot, for example.
If you are working with client/server apps, chances are you would want some sort of asynchronous processing at one time or another. Official projects like Ktor, for example, embrace coroutines from scratch to provide the best way to build your objectives into code.