Mastering Asynchronous Programming with Kotlin Coroutines — Part 1

Kamna Garg
Nerd For Tech
Published in
6 min readMar 25, 2024

--

Welcome to mastering Asynchronous Programming with Kotlin Coroutines — Part 1. In this article, we’re going to cover coroutine basics, coroutine usage, and coroutine builders launch, async, and runBlocking.

Introduction to Coroutines

When an app starts, it initiates a main thread responsible for handling lightweight tasks like button clicks or user login. However, if the app needs to execute a lengthy operation such as downloading a file, or network calls, doing so on the main thread can cause the app to become unresponsive, leading to a poor user experience. To counter this situation, we should run background threads to handle these tasks. However, as each thread consumes a significant amount of memory, running a lot of background threads can lead to out-of-memory errors.

Here comes coroutines to the rescue.

Coroutines act as lightweight threads, offering a more efficient solution compared to traditional threads. They are designed to be cheap and consume minimal memory. One of the key advantages of coroutines is their ability to be launched on a single thread and perform multiple operations without blocking other coroutines.

Multiple threads performing one operation at a time

In the above diagram, multiple threads are launched to perform various operations. Below is the code to create threads in Kotlin using thread keywords.

Main thread starts here : main
Main thread ends here : main
Fake work starts here : Thread-0
Fake work finished here : Thread-0

One thing to note here is that although the main thread has finished its work it will still wait for other threads to finish the work.

Multiple coroutines performing operations on a single thread
Main thread starts here : main
Main thread ends here : main

So, it’s evident that coroutines enable asynchronous execution without blocking the main thread. However, in this scenario, we didn’t achieve the desired outcome from the coroutine. Although we launched a coroutine using the createCoroutine function, it didn't print anything. We need the main thread to wait until the execution of all the coroutines is completed.

  1. One simple solution is to deliberately add a delay in the main thread using thread.Sleep()to ensure that the coroutine finishes its work. But, this is an impractical solution as we can’t always predict the time required for the coroutine to finish its task. Blocking the main thread with a fixed delay is also not efficient and may lead to unnecessary waiting or potential responsiveness issues in the application.
Main thread starts here : main
Fake coroutine starts here : DefaultDispatcher-worker-1
Fake coroutine finished here : DefaultDispatcher-worker-1
Main thread ends here : main

2. We can use thread.join call to wait for all coroutines to finish the work before the main thread terminates.

In the above code snippet, we’ve used job.join() instead of the sleep() function.

Let’s move to the next topic now how to create coroutines in our application.

How to create Coroutines

Coroutines in Kotlin are created using Coroutine builders. These are functions or constructs provided by Kotlin’s coroutine library that allow the creation and management of coroutines. These builders simplify the process of launching and managing coroutines, providing different options based on specific use cases.

Some common coroutine builders include:

Common Coroutine Builders

Before going into how to use these builders to create coroutines, let’s understand the concept of scopeof these builders first.

In Kotlin, when dealing with coroutines, scope refers to the context in which a coroutine runs and is controlled. There are primarily two types of scopes relevant to coroutines: GlobalScope and CoroutineScope.

  1. GlobalScope: It is a top-level scope that is not tied to any specific lifecycle or context. Coroutines launched in global scope continue to execute until they are complete or until the application terminates. It’s generally recommended to avoid using GlobalScope in production code because coroutines launched in GlobalScope can potentially run indefinitely and may lead to resource/memory leaks or unintended behavior.
  2. CoroutineScope: This is a scope tied to a specific coroutine builder, such as launch or async. When the associated object is destroyed or when the scope is canceled, all coroutines launched within that scope are automatically canceled. When the thread is closed, all the coroutines associated with that thread are also closed/destroyed.

Launch Builder

The launch builder launches a coroutine having a return type job . This job object can be used to perform various other operations like join and cancel (which will be discussed in the next part).

Here, we use GlobalScope.launch to create a coroutine with a global scope. Alternatively, we can use launch to create a coroutine with a coroutine scope.

Main thread  starts here : main
Fake coroutine starts here : DefaultDispatcher-worker-1
Fake coroutine finished here : DefaultDispatcher-worker-1
Main thread ends here : main

Here, we’ve used the delay() function instead of Thread.sleep() in the doWork() function. The difference between the delay() and sleep() functions will be discussed later in this article.

Pros

  • Lightweight and efficient for fire-and-forget tasks.
  • No overhead in managing a result.

Cons:

  • Lack of result handling may complicate error management.
  • Careful usage is required to avoid resource leaks.

Async Builder

The asyncbuilder creates a coroutine that computes a result asynchronously and returns a deferred result just like the future in other programming languages. You need to use await() function to retrieve the corresponding result.

Main thread  starts here : main
Fake coroutine (Join) starts here : main
Fake coroutine (Join) finished here : main
Job type is deffered job
Main thread ends here : main

In this example, we retrieve the result using deferred.await() call, where the return type can be of any data type. Instead of thread.sleep , we have used join function call to wait for all coroutines to finish the work before the main thread terminates.

Pros:

  • Facilitates multiple concurrent computations with easy result retrieval.

RunBlocking Builder

The runBlocking coroutine builder creates a new coroutine and blocks the current thread until its execution is complete. It is typically used in testing, main functions, or blocking code that needs to be integrated into coroutine-based systems. It is mainly used to test the suspending functions (details of run blocking will be covered in the next part of the blog alongside suspending functions). For now just remember — It can only be called by coroutines and suspend functions.

In this example, the fetchData function is a suspending function and a suspend function can only be called by the coroutines or suspend functions. We are using runBlockinghere to create a coroutine to test fetchData function.

Difference between Sleep and Delay function

The delay function is an alternative to the sleep function because thread.sleep()makes the entire thread sleep rather than blocking the corresponding coroutine only.

The delay function is a type of suspend function, that allows us to pause the execution of a coroutine for a specified amount of time without blocking the underlying thread.

Difference between sleep and delay function

In the above diagram, coroutine c1 has calledthread.sleep function but it has suspended the main function and all the coroutines associated with it. But delay function has suspended the execution of coroutine c1 only.

In conclusion, Kotlin coroutines offer an efficient solution for asynchronous programming, providing lightweight threads and simplifying concurrency. Coroutine builders like launch, async, and runBlocking facilitate various use cases, but care must be taken to avoid blocking issues and resource leaks. Asynchronous code can be tested synchronously using runBlocking, enhancing simplicity and ease of testing in coroutine-based systems.

Thank you for taking the time to read. I hope you found it insightful and engaging. Keep an eye out for the next parts!

--

--

Kamna Garg
Nerd For Tech

Software Developer, Women in tech, Seeker, Love writing, Always a student, IIT Kanpur