Mastering Kotlin Coroutines – Part 1

Lets understand basic concept behind Program, process and thread.

Program: A program is a set of instructions designed to perform a specific task or a set of tasks.

Process: A process is an instance of a program that is being executed by the operating system.

  • When a program is executed, the operating system creates a process for it.
  • Each process has its own memory space, which includes the program’s code, data, and resources (such as files, network connections, and hardware resources).
  • Processes are managed by the operating system, which allocates resources, schedules their execution, and provides protection mechanisms to prevent interference between processes.

Thread: A thread is the smallest unit of execution within a process.

  • Processes can have one or multiple threads, each capable of executing independently and concurrently with other threads within the same process.
  • Threads share the same memory space and resources within a process, including the code, data, and resources allocated to the process.

Sequential execution

Every instruction in program or process runs sequentially as shown below

But suppose Instruction 2 is time consuming operation and will take longer time ( eg reading database or fetching data from API ) to execute then it will halt the execution of Instruction 3 and so on. Another problem with long running operation is until the task is busy reading database or fetching data from API, the thread will be idle and wait for Instruction 2 to complete. Hence CPU utilisation will not proper.

We can solve these problems by identifying that Instruction 2 is independent task and no other tasks depends on this then we can run Instruction 2 separately in a new thread as shown below and existing thread will continue executing next instructions in queue. This way we can utilise our CPU effectively without halting any Instructions.

Lets understand thread and Long running or Blocking operations little more

Assume we have 2 threads running instruction sequentially,

Thread 1 – At point [A] an instruction came to fetch data form API and its receives data from API at point [B] so from point [A] to [B] it was waiting to receive response from server hence Thread 1 was idle and in waiting state.

Similarly Tread 2 at point [C] an instruction came to read files from file system and its complete reading at point [D] so from point [C] to [D] it was waiting to reading files to complete hence Thread 2 was idle and in waiting state.

So we can say that In this approach CPU utilisation was not effective and there were some points [A to B] and [C to D] when threads were not performing any task and sitting idle in waiting state. Can we reutilise these idle threads ?

Can we reutilise these idle threads ? this set up the foundation of coroutine.

From above picture from points [A to B] and [C to D] when threads were not performing any task and sitting idle in waiting state, during these time we will utilise these threads to perform some other tasks. This way we can effectively utilise the CPU.

In above picture you can see Thread pool – A thread pool is a collection of pre-initialized threads that are available for executing tasks.

Suppose we want to execute a program consisting Task 1, I/O operation and Task 2.

Lets breakdown the events

-> OS system will pick a thread from thread pool and assign to that program’s process.

–> Thread will start executing task 1 and after completing, it will initiate I/O operation which could take more time during that time thread will be in waiting state so till itis waiting, system will free it up and send back to thread pool available to perform any other task

When I/O operation will completes then time to perform task 2 so OS will check for available threads in thread pool, if same previous thread is not available then OS will pick another thread to perform task 2.

This way we properly utilise threads with blocking or waiting the threads.

I hope till now you all are following 🙂

Let’s breakdown Coroutine

Coroutine equals to Co + Routine, Co means coperative and Routine means function. So Coroutines are functions which run inside thread in a co-operating manner to utilise thread pool effectively and minimizing the requirement to create multiple new threads. Application tries to complete the tasks within the provided set of threads. We can run multiple task with the help of multiple coroutine inside a single thread. Creating coroutines is cheaper than creating threads. There is limitation in creating threads as they consume signification amount of memory, but we can create any number of coroutine and all coroutines can runs within a single thread.

understand with one more example

Assume we have two tasks so we have created a two courting for these tasks as show in below picture and now let’s see how they will execute on a single thread. A single thread is requested form thread pool and Coroutine 1( C1)start executing with task 1 (T1). After completing T1 an IO operation came then C1 suspended at this point and free thread, then T4 from C2 start executing. After completing T4 another IO operation of C2 came so C2 also suspended at this point and free thread and same time IO operation of C1 completed. Thread again picked C1 and resume where it was suspended so now T2 start executing and this process continues till T3 is completed. This is how two coroutines runs together on a same thread.

Coroutines

  • Coroutines works like thread but they are not thread. Coroutines are called light weight thread as they mimic the behaviour of thread. Coroutines works on top of thread so basically Coroutines is a framework, so we developers interact with this framework and this framework behind the scene interact with threads.

Coroutines Scope

Coroutine scope defines the boundary, lifecycle and cancellation behavior of coroutines, ensuring that coroutines launched within the scope are automatically canceled when the scope is canceled, thereby facilitating proper resource management and preventing memory leaks.

Coroutines Dispatcher

n Kotlin Coroutines, a dispatcher is responsible for determining which thread or thread pool a coroutine will execute on. Dispatchers abstract away the complexity of managing threads, allowing developers to focus on writing asynchronous code without worrying about low-level threading details.

  1. Dispatchers.Default: This dispatcher is optimized for CPU-bound tasks. It uses a shared pool of threads, typically equal to the number of CPU cores available on the system. It’s suitable for CPU-intensive computations and blocking I/O operations.
  2. Dispatchers.IO: This dispatcher is optimized for I/O-bound tasks, such as network or disk I/O operations. It uses a pool of threads designed to perform non-blocking I/O operations efficiently. Using this dispatcher ensures that I/O operations do not block the main thread, keeping the application responsive.
  3. Dispatchers.Main: This dispatcher is specifically designed for Android applications. It represents the main/UI thread of the Android application. It should be used for UI-related operations, such as updating UI components, because Android requires UI updates to be performed on the main thread.

Example:

Suppose we have an Android application where we need to fetch data from a network API and update the UI with the result. We’ll use coroutine scope to launch a coroutine and a coroutine dispatcher to specify that the network request should be performed on the IO thread, while the UI update should be performed on the main thread.

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

// Define a coroutine dispatcher for IO operations
val ioDispatcher: CoroutineDispatcher = Dispatchers.IO

// Define a coroutine scope for the activity
val activityScope = CoroutineScope(Dispatchers.Main)

fun fetchDataFromNetwork(): String {
    // Simulate a network request
    Thread.sleep(2000)
    return "Data from network"
}

fun updateUI(data: String) {
    // Update UI with the fetched data
    println("UI updated with data: $data")
}

fun main() {
    // Launch a coroutine within the activity scope
    activityScope.launch {
        println("Coroutine started on thread: ${Thread.currentThread().name}")

        // Perform network request on the IO dispatcher
        val data = withContext(ioDispatcher) {
            println("Network request performed on thread: ${Thread.currentThread().name}")
            fetchDataFromNetwork()
        }

        // Update UI on the main thread
        updateUI(data)
    }

    // Simulate the main thread continuing to execute
    println("Main thread continues...")

    // Keep the program running to observe coroutine execution
    readLine()
}

In this example:

  • We define a coroutine dispatcher (ioDispatcher) for IO operations, specifying that network requests should be performed on the IO thread.
  • We define a coroutine scope (activityScope) using Dispatchers.Main to ensure that coroutines launched within this scope execute on the main thread, suitable for UI updates.
  • Inside the main() function, we launch a coroutine within the activityScope.
  • Within the coroutine, we use withContext to switch to the IO dispatcher and perform the network request asynchronously.
  • Once the data is fetched, we switch back to the main dispatcher using withContext and update the UI.
  • The main thread continues executing other tasks while the coroutine performs the network request asynchronously.

The output of the provided code will be:

Main thread continues...
Coroutine started on thread: main
Network request performed on thread: DefaultDispatcher-worker-1
UI updated with data: Data from network

Here’s a breakdown of the output:

  • The main thread continues executing while the coroutine is launched.
  • The coroutine starts executing on the main thread.
  • The network request is performed on a background thread (DefaultDispatcher-worker-1), as specified by the IO dispatcher.
  • Once the network request is complete, the UI is updated with the fetched data on the main thread.