Kotlin Coroutines from ground up

The asynchronous form of programming is quite famous in this fast-moving world where everything needs to get done as quickly as possible. Every programming language has its own way to achieve this feast. Kotlin achieve this feast using coroutines.

Coroutines are light-weight threads that allow you to write asynchronous non-blocking code

Kotlin Documentation

In this blog we will go into detail on how CPU does parallelism, software parallelism, threads, then compare with the coroutines, advantage of coroutines, implementation, and cancellation of coroutines.

CPU and Core

CPU (Central processing unit), yes you are right to understand coroutine let’s start from the basics, as we know all the processes (Gaming, browsing, media, code execution) are eventually converted into a set of instructions and these instructions are executed by the CPU. So what are these instructions at a very basic level?, a CPU can perform only a handful of instructions. For example, the instruction set of x86 is as follows.

https://en.wikipedia.org/wiki/X86_instruction_listings

When executing a set of instructions following things is also required

  1. Storage to store all the instructions in the sequence
  2. A counter to keep track of the index of the executed instruction
  3. Storage to save the result of any instructions

In a CPU these are fulfilled by L1 Cache, L2 Cache, RAM, and Registers.

To summarise, “To run a set of instructions we also need some level of metadata in form of counter, to store results and to use it again”

We can say a thread is a collection of these instructions, so when there are 2 threads. Each thread will have its own set of these data which are:

  1. Program Counter – to keep track of the current instruction being executed by the thread
  2. A Stack – To store local variables, function parameters, and other data used by the thread. When a function is called, its arguments are pushed onto the stack, and when the function returns, its result is popped off the stack.
  3. Registers – Registers are used to store the intermediate results of the thread, these registers are loaded to CPU registers.

When a thread is scheduled to run on the CPU Core, these details of the thread are also loaded into the CPU register and memory.

To summarise,

  1. CPU executes streams of instruction, which is called a thread.
  2. In another word, what is a thread? Thread is a collection of instruction set with metadata and stack for execution.

Concurrency

In computer science, concurrency is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in a partial order, without affecting the outcome.

Wikipedia

Consider a single-core CPU, which has to execute 4 threads, the scheduler, while scheduling a thread don’t wait for the completion of that thread, instead, it keeps switching the thread which gives a sense of parallelism but in reality only a single thread or set of instructions are getting executed in a single core CPU. This sense of parallelism is called concurrency.

A high level of concurrency can be achieved through hardware modification or software

Hardware Concurrency:

Concurrency can be improved by increasing the number of cores in the CPU or the number of threads supported by the CPU. More the cores more threads will be executed at the same time.

Software Concurrency:

Software concurrency refers to the use of programming techniques to enable multiple tasks to be executed simultaneously in a single process or program. Unlike hardware concurrency, which uses multiple CPUs or cores to achieve parallelism, software concurrency uses techniques such as threads, coroutines, or event-driven programming to enable parallel execution within a single process.

Native threads vs Java Threads

Native threads or OS threads as discussed early are a set of instructions with their data which is managed by the Operating System.

Java Threads:

Java threads are lightweight user-level threads that are managed by the Java Virtual Machine (JVM). The JVM manages threads in user space, which means that the threads are implemented entirely in the Java runtime library, rather than in the operating system kernel but these threads are scheduled and executed by the Operating system

Coroutine

Coroutines can be used to implement cooperative functions. Instead of being run on kernel threads and scheduled by the operating system, they run in a single thread. They allow developers to write code that can run tasks concurrently, without the overhead of creating separate threads because of this coroutines are light weight and better than threads, in kotlin, the coroutine is managed by the Kotlin runtime.

Coroutine builders

Coroutine builders are the functions which allow us to create a coroutine. Following are the main coroutine builders present in kotlin.

Launch

fun main () {
 println("Tasks executed in main thread")
  GlobalScope.launch {
    println("Tasks executed in coroutine by same or other thread")
 }
 println("Main thread completed")
}

The launch coroutine builder have following properties

  • Doesn’t block the parent thread.
  • Inherit parent coroutine’s scope and thread
  • Returns a job object
  • Allow provision to cancel and wait for the job to complete.

Difference between GlobalScope.launch vs launch lambda {}

As the name suggest GlobalScope creates a coroutine at application level whereas launch {} creates a coroutine in local scope. Consider an android application with login page and a coroutine is launched from this page. Incase of local scoped coroutine as soon as the Login page is destroyed the coroutine is also gets destroyed. Coroutines in GlobalScope can live till the application lives.

Using a Global scope considered as a bad practice because of obvious reasons.

2. Async

async coroutine builder is similar to the launch coroutine builder with a small difference.

Async coroutine builder have following properties.

  • Doesn’t block the parent thread.
  • Inherit parent coroutine’s scope and thread
  • Returns a DeferredJob<T> object
  • Allow provision to cancel and wait for the job to complete.
  • Allow provision to get the result of coroutine
fun main () = runBlocking {
 println("Tasks executed in main thread")
  val result: DeferredJob<Int> = async {
    println("Tasks executed in coroutine by same or other thread")
   return 20
 }
 val ans = result.wait() // To fetch result
 result.cancel() // to cancel the coroutine
 result.join() // to wait for the coroutine to complete
 println("Main thread completed")
}

3. runBlocking

runBlocking is different in nature compare to above mentioned coroutine builders, runBlocking doesn’t suspend thread but blocks the thread.

runBlocking generally used when you want to launch a coroutine in non suspending functions or outside the coroutine. So the general use case is when you want to write test cases of your code.

4. withContext

withContext is similar to ruBlocking as it also suspends the coroutine from executing the next piece of code.

suspend fun test() {
  println("Started")
  withContext(Dispatchers.IO) {
   for(i in 0.500) {
    print(i)
   }
 }
 println("END") // not gets executed till above loop gets completed
}

Cancelling a coroutine

Coroutine provide a feature to stop its execution which is cancelling a coroutine, so why you want to cancel a coroutine consider were the coroutine taking lot of time to execute or you don’t want the result of that coroutine. In this case a coroutine can be cancelled, to cancel a coroutine it should be corporative.

what we mean by coroutine to be coorperative?

So not all coroutines can be cancelled, to make your coroutine cancellable you can do one of these 2 things.

Use any suspending function from kotlinx.coroutines library

func main () = runBlocking {
 println("Execution started")

 val job: Job = launch {
   for(i in 1..1000) {
    print(i)
    delay(1) // Internally checks if the Coroutine is active
   }
  }
  
  job.cancel()
}

Here the delay function will return if the coroutine gets cancelled before executing any further.

2. Check if the Coroutine is active by yourself.

Every coroutine scope have isActive flag, which is to identify if a coroutine is got cancelled.

func main () = runBlocking {
 println("Execution started")

 val job: Job = launch {
   for(i in 1..1000) {
    print(i)
    if (!isActive) {
      return
    }
   }
  }
  
  job.cancel()
}

Related Post

Leave a Reply

Your email address will not be published. Required fields are marked *