1. Overview

We can think about Kotlin coroutines as lightweight threads. They allow us to execute code concurrently and are less resource-consuming than threads. Using coroutines can improve the performance, responsiveness, and scalability of our applications.

In this tutorial, we’ll explore different ways to run multiple coroutines in parallel in Kotlin.

2. Run Coroutines in Parallel Using launch()

Let’s take a look at how we can use the launch() function to start two coroutines in parallel:

suspend fun executeTwoCoroutinesInParallelUsingLaunch() {
    coroutineScope {
        launch { doSomething() }
        launch { doSomethingElse() }
        println("Waiting for coroutines to finish")
    }
}

suspend fun doSomething() {
    val delay = Random.nextInt(100, 1000)
    delay(delay.milliseconds)
    println(" - doSomething waited for $delay milliseconds")
}

suspend fun doSomethingElse() {
    val delay = Random.nextInt(100, 1000)
    delay(delay.milliseconds)
    println(" - doSomethingElse waited for $delay milliseconds")
}

Let’s break down all the important parts in the example:

  • coroutineScope(): creates a new coroutine scope and runs the specified block of code within that scope
    • If any child coroutine fails, this whole scope fails and all the child coroutines are canceled
    • It returns when the given code block and all child coroutines are completed, following the structured concurrency principle
  • launch(): launches a new coroutine that can be executed in parallel with other coroutines and returns a reference to the execution as a Job object
  • doSomething() and doSomethingElse(): wait for a random delay, without blocking the main thread, then log how long they waited for and return

Now, let’s execute the function and record how long it takes to complete:

val executionTime = measureTimeMillis {
    executeTwoCoroutinesInParallelUsingLaunch()
}
println(" - The whole example took $executionTime milliseconds")

Every time we execute this example, the output will be different — sometimes, doSomething() completes first:

Waiting for coroutines to finish
 - doSomething waited for 309 milliseconds
 - doSomethingElse waited for 939 milliseconds
 - The whole example took 953 milliseconds

And other times, doSomethingElse() finishes first:

Waiting for coroutines to finish
 - doSomethingElse waited for 564 milliseconds
 - doSomething waited for 581 milliseconds
 - The whole example took 584 milliseconds

Looking at the total execution time, we can clearly see that the coroutines were executed concurrently since the execution time for the whole example is significantly shorter than the execution times of the two coroutines combined.

The Job returned by launch() provides lots of functionalities for managing asynchronous code execution. For instance, it allows waiting for the completion or cancellation of the coroutine. All the details for the Job lifecycle can be found in the official Kotlin documentation.

3. Execute Coroutines in Parallel Using async()

In case we need to process the results, we can make use of the async method:

suspend fun executeTwoCoroutinesInParallelUsingAsync() {
    coroutineScope {
        val something = async { fetchSomething() }
        val somethingElse = async { fetchSomethingElse() }

        println("Waiting for coroutines to finish")
        println(
            "The sum of something and somethingElse " +
                "is: ${something.await() + somethingElse.await()}",
        )
    }
}

suspend fun fetchSomething(): Int {
    val delay = Random.nextInt(100, 1000)
    delay(delay.milliseconds)
    println(" - fetchSomething waited for $delay milliseconds")
    return delay
}

suspend fun fetchSomethingElse(): Int {
    val delay = Random.nextInt(100, 1000)
    delay(delay.milliseconds)
    println(" - fetchSomethingElse waited for $delay milliseconds")
    return delay
}

The function async() returns a Deferred<Int>. The coroutine will execute in the background, and we can obtain the actual return value with the await() function. Note that await() is a suspend function, so it won’t block the thread that is executing, and it will suspend the current coroutine until the result is actually available.

If we execute the code snippet:

val executionTime = measureTimeMillis {
    executeTwoCoroutinesInParallelUsingAsync()
}
println(" - The whole example took $executionTime milliseconds")

The output will be:

Waiting for coroutines to finish
 - fetchSomething waited for 588 milliseconds
 - fetchSomethingElse waited for 678 milliseconds
The sum of something and somethingElse is: 1266
 - The whole example took 697 milliseconds

4. Execute Many Coroutines in Parallel

If we need to execute multiple tasks in parallel and continue only when we have all the results, we can make use of the awaitAll() function.

Let’s take a look at an example that returns the list of all the numbers from 1 to 5 multiplied by themselves:

suspend fun executeManyCoroutinesInParallelUsingAsync(): List<Int> {
    val result = coroutineScope {
        (1..5).map { n ->
            async {
                val delay = Random.nextInt(100, 1000)
                delay(delay.milliseconds)
                println("- processing $n")
                n * n
            }
        }.awaitAll()
    }
    println("Result: $result")
    return result
}

The output of the code above is:

- processing 3
- processing 5
- processing 1
- processing 4
- processing 2
Result: [1, 4, 9, 16, 25]

All the coroutines are processed independently and concurrently, but then we’re able to wait for the results, thereby maintaining the original order.

Kotlin provides two implementations of awaitAll() — one as an extension function, as shown in the example above, and one that takes in a variable number of Deferred objects. The implementations are equivalent, but they may allow for cleaner code according to our use case.

5. Conclusion

In this article, we learned a few ways to run multiple coroutines in parallel. Coroutines are lightweight threads that allow us to execute concurrent code without blocking threads. This can improve the performance, responsiveness, and scalability of our applications.

We can use the launch() and async() functions to run coroutines in parallel. The launch() function returns a Job object that allows us to manage the coroutine, while the async() function returns a Deferred object that can be used to get the result of the coroutine when it is complete.

We can use the awaitAll() function to wait for the results of multiple coroutines in parallel. This is useful when we need to execute numerous similar tasks and then continue only when we have all the results.

As always, the code for these examples is available over on GitHub.

Comments are closed on this article!