Efficient concurrent programming with Kotlin coroutines

Siddharth Rawat
5 min readMay 25, 2023

--

Introduction

Concurrency is a crucial aspect of modern software development, allowing programs to perform multiple tasks concurrently, thereby improving overall efficiency and responsiveness.

Kotlin, a versatile programming language built on the Java Virtual Machine (JVM), offers powerful features and libraries for concurrent programming. In this article, we will explore how can we write efficient concurrent programs to achieve better performance.

Kotlin Coroutines

Kotlin coroutines are a way to write asynchronous code that looks like synchronous code, allowing for easier concurrency and non-blocking operations, similar to delegating tasks to assistants while you focus on other things.

To understand this more vividly from a real life situation, imagine you’re a chef working in a busy restaurant kitchen. You have to handle multiple orders simultaneously while ensuring a smooth workflow. However, you don’t want to get stuck waiting for one dish to finish cooking before moving on to the next. This is where Kotlin coroutines come in.

Coroutines are like sous chefs in your kitchen. Instead of you waiting for each dish to cook, you can delegate the task to a sous chef and move on to preparing the next dish. The sous chef works independently and notifies you when the dish is ready.

In programming terms, coroutines are lightweight threads that allow you to perform tasks concurrently without blocking the main thread.

With coroutines, you can efficiently handle multiple operations, such as fetching data from a server, performing calculations, or updating the user interface, without freezing the app or blocking the user’s interaction.

Concurrent execution with coroutines

In this example, we have three coroutines executing concurrently. job1 and job2 use the launch builder to perform their tasks with a delay of 1000ms each. job3 uses the async builder and returns a result after a delay of 1000ms.

While these coroutines are executing, we print “Performing other tasks while coroutines are executing…” to simulate other work being done in the meantime.

The joinAll function is used to wait for all the coroutines to complete before proceeding. We then print the result of job3 using await() to retrieve the value produced by the coroutine.

You’ll notice that even though each individual job take around 1000ms to finish their task, the program finishes in just over 1000ms. This example demonstrates how coroutines allow for efficient concurrent execution without blocking the main thread, resulting in faster execution of the program.

Dispatchers

We know coroutines need thread(s) for their execution. Though we don’t explicitly create them, it doesn’t mean they aren’t used.

Dispatchers are responsible for determining which thread or thread pool is used to execute a coroutine. They provide the execution context for coroutines, specifying where and how the coroutine runs.

Kotlin supports several built-in dispatchers, each with its purpose but we wont dive into it as it’s going to increase the scope.
By using different dispatchers, you can control where coroutines are executed and choose the appropriate dispatcher based on the nature of the task; whether it’s UI-related, CPU-intensive, or I/O-bound.

How not to write coroutines

Writing coroutines doesn’t guarantee concurrent execution if the underlying code being delegated itself is thread-blocking in nature.

A thread-blocking function is one that performs a long-running or blocking operation that may potentially block the underlying thread. Examples of blocking operations are network I/O, database operation, etc.

When using coroutines in Kotlin, the goal is to achieve asynchronous, non-blocking code execution.

Let us understand this from the code snippet below:

You’ll notice that even though we use coroutines, the total execution time is greater than 3000ms. You will get the similar execution time even if you executed the job sequentially with no coroutines.

Why did this happen ?

Well, if you notice the console logs, you will see that all the 3 jobs are executed in the Main thread. Since our function performBlockingOperation()is thread blocking in nature, it essentially blocks the execution of any other job until one finished up. So job1, job2, and job3 are executed one after the other and not concurrently.

How to write coroutines efficiently

To avoid these issues, Kotlin provides alternatives for performing blocking operations from coroutines. These alternatives include using suspending functions or running the blocking code in a separate thread pool.

By using suspending functions, you can perform the blocking operation in a non-blocking manner, allowing the coroutine to suspend and free up the underlying thread for other tasks.

Kotlin provides following mechanisms to handle blocking operations from within coroutines:

1. Switch to a different threadpool

Using withContext(Dispatchers.IO) or other appropriate dispatchers to switch to a different thread pool optimized for I/O operations. This allows the coroutine to suspend and avoid blocking the main thread or other critical threads.

In this example, withContext(Dispatchers.IO) block is used to switch to the I/O dispatcher, which is optimized for I/O operations and frees up the main thread. Inside this block, the performBlockingOperation() function is called which executes on the Dispatchers.IO thread pool without blocking the main thread. We can also see from the console logs, that the 3 jobs are not executed on the Main thread.

The total execution time taken is around 1000+ms.

2. Using suspending functions

In Kotlin coroutines, a suspend function is a function that can be paused and resumed later without blocking the calling thread. It is designed to perform non-blocking operations, such as I/O or long-running computations, without blocking the execution of the program.

In this example, the performBlockingOperationSuspended() function is defined as a suspending function by using the suspend keyword. Inside this function, delay(1000) is used to suspend the coroutine for 1 second.

We can also see from the console logs, that all the 3 jobs are executed on the Main thread itself but since delay() is a suspending function, it doesn't block the underlying thread but instead suspends the coroutine. The execution of the coroutine resumes after the delay.

The total execution time taken is around 1000+ms as well.

Conclusion

In summary, Kotlin coroutines are like sous chefs that help you handle multiple tasks concurrently, freeing you up to work on other tasks while they complete their assignments. They are lightweight threads that can be suspended and resumed without blocking the underlying thread.

To make the most out of Kotlin coroutines, it is crucial to understand how to use them efficiently. By leveraging suspending functions, proper use of dispatchers, and structured concurrency, you can ensure that your code performs well and is easier to manage.

This article has provided an overview of Kotlin coroutines and their benefits for concurrent programming. While there is much more to explore and learn about coroutines, the intention of this post was to raise awareness and get developers started with Kotlin’s concurrent programming capabilities.

I hope this article has been helpful in expanding your knowledge of Kotlin coroutines and their role in concurrent programming.

Happy coding!

--

--