Home Mastering Asynchronous Programming with Kotlin Coroutines — Part 1
Post
Cancel

Mastering Asynchronous Programming with Kotlin Coroutines — Part 1

Mastering Asynchronous Programming with Kotlin Coroutines — Part 1

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

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.

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
    println("Main thread starts here : ${Thread.currentThread().name}")

    // Launching a new thread to offload work from the main thread
    thread {
        println("Fake work starts here : ${Thread.currentThread().name}")
        Thread.sleep(1000) // some fake work like file downloading or n/w call etc. 
        println("Fake work finished here : ${Thread.currentThread().name}")
    }

    println("Main thread ends here : ${Thread.currentThread().name}")
}
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

Multiple coroutines performing operations on a single thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
    println("Main thread starts here : ${Thread.currentThread().name}")
    // Launching a new coroutine to offload work from the main thread
    createCoroutine()
    println("Main thread ends here : ${Thread.currentThread().name}")
}

fun createCoroutine() {
    // operates with in a thread
    GlobalScope.launch {
        println("Fake coroutine starts here : ${Thread.currentThread().name}")
        Thread.sleep(1000) // some fake work
        println("Fake coroutine finished here : ${Thread.currentThread().name}")
    }
}
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
    println("Main thread starts here : ${Thread.currentThread().name}")
    // Launching a new coroutine to offload work from the main thread
    createCoroutine()
    Thread.sleep(2000)
    println("Main thread ends here : ${Thread.currentThread().name}")
}

fun createCoroutine() {
    // operates with in a thread
    GlobalScope.launch {
        println("Fake coroutine starts here : ${Thread.currentThread().name}")
        Thread.sleep(1000) // some fake work
        println("Fake coroutine finished here : ${Thread.currentThread().name}")
    }
}
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() = runBlocking { // this: CoroutineScope
        println("Main thread  starts here : ${Thread.currentThread().name}")

        val job: Job = GlobalScope.launch {// it can launch or GlobalScope.launch based on the requirement
            println("Fake coroutine starts here : ${Thread.currentThread().name}")
            doWork(1000)
            println("Fake coroutine  finished here : ${Thread.currentThread().name}")
        }
        Thread.sleep(2000)
        job.join() // wait for the coroutine to finish
       // job.cancel() // cancel the coroutine
        println("Main thread ends here : ${Thread.currentThread().name}")
    }
suspend fun doWork(time : Long) {
    delay(time) // some fake work
}

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

Common Coroutine Builders

Before going into how to use these builders to create coroutines, let’s understand the concept of scope of 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main() = runBlocking { // this: CoroutineScope
        println("Main thread  starts here : ${Thread.currentThread().name}")

        val job: Job = GlobalScope.launch {// it can launch or GlobalScope.launch based on the requirement
            println("Fake coroutine starts here : ${Thread.currentThread().name}")
            doWork(1000)
            println("Fake coroutine  finished here : ${Thread.currentThread().name}")
        }
        Thread.sleep(2000)
        job.join() // wait for the coroutine to finish
     // job.cancel() // cancel the coroutine
        println("Main thread ends here : ${Thread.currentThread().name}")
    }

suspend fun doWork(time : Long) {
    delay(time) // some fake work
}
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 async builder 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() = runBlocking { // this: CoroutineScope
    println("Main thread  starts here : ${Thread.currentThread().name}")

    val deferred: Deferred<String> = async {
        println("Fake coroutine (Join) starts here : ${Thread.currentThread().name}")
        doWork(1000)
        println("Fake coroutine (Join) finished here : ${Thread.currentThread().name}")
        "deffered job"
    }
    val jobType: String = deferred.await() // wait for the coroutine to finish and returns the result
    deferred.join() // wait for the coroutine to finish
    println("Job type is $jobType")
    println("Main thread ends here : ${Thread.currentThread().name}")
}

suspend fun doWork(time : Long) {
    delay(time) // some fake work
}
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.

1
2
3
4
5
6
7
8
9
10
11
12
class RunBlockingTest {
    @Test
    fun `test fetchData`() = runBlocking {
        val result = fetchData()
        Assert.assertEquals("Mock data", result)
    }
}

suspend fun fetchData(): String {
    // Simulate fetching data asynchronously
    return "Mock data"
}

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 runBlocking here 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

Difference between sleep and delay function

In the above diagram, coroutine c1 has called thread.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!

Post converted from Medium by ZMediumToMarkdown.

This post is licensed under CC BY 4.0 by the author.