Experimenting with Kotlin Coroutines
As I mentioned previously, I've started on a new project that uses Kotlin heavily. I don't have extensive experience with Kotlin yet, but I'm learning a lot and love it. I recently needed to submit multiple HTTP requests concurrently. There were a few examples in the codebase that I had come across, but I never had the need to dig deeper. The existing code used Kotlin Coroutines. I'm familiar with Threads in Java and concurrent programming—I've even done a side project using Ratpack—but Kotlin Coroutines were fairly new to me.
Real quick, for those that don't know, Kotlin Coroutines allow one to create asynchronous programs in a fluent way; they can be thought of as light-weight threads. The existing code looked something like this (simplified for clarity and to protect the innocent):
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
suspend fun fetch(): MyClass =
coroutineScope {
val data1 = async { fetchData1() }
val data2 = async { fetchData2() }
MyClass(data1.await(), data2.await())
}
What I needed was something similar, but my use case didn't require returning a result. I needed to fetch a result containing a list of values, and submit a request for each item concurrently without waiting for a result, but wait for all requests to finish. I searched the internet for simple examples, but almost everything was too long or confusing. And in my opinion, the best way to learn something new is to just play around with it.
So I created a simple Kotlin Scratch file in IntelliJ Idea to experiment. I'm including a screenshot below so you can see the output side-by-side, which should make it easier to understand. See this gist if you want to copy the code and play around with it in Idea. Note this will require you add the necessary kotlinx.coroutine dependencies and make them available on your scratch file's classpath. Or if you want to play around with it online, I also copied the code to Kotlin's Playground.
A few things I learned about Kotlin Coroutines:
- Use
launch
when you don't need to return a result. Also,launch
returns an instance ofJob
which allows you to calljoin
if you do need to wait till it completes. Or use something likelistOf(job1, job2).joinAll()
if you need to explicitly wait for all jobs to complete. - Use
async
when you do need to return a result. It returns an instance ofDeferred
, which extendsJob
. You can call theawait
function when you need to wait till it completes. - Calling
launch
/async
will start that coroutine concurrently immediately; no need to call astart
method or anything, like you have to do with a Java Thread. - All the jobs will be complete by the time the program exits
coroutineScope
orrunBlocking
. This was one of my biggest questions: did I have to explicitly calljoin
/await
to ensure all jobs were complete before returning from my function? And the answer is no. For example, notice in my scratch's output thefinished
statement is last and isn't emitted until all the jobs are complete. This makes sense since I am callingrunBlocking
, but I assume the same is true when usingcoroutineScope
and that aligns with the Kotlin docs for coroutineScope (which to me were initially confusing).
Anyways, I feel a little more knowledgeable about Kotlin Coroutines. Still a ton more to learn, but that will come with time. Hope it helps someone else.