How Coroutines Enable Cooperative Multitasking and Concurrency

Written by jesperancinha | Published 2024/03/18
Tech Story Tags: kotlin | coroutines-in-kotlin | concurrency | coroutines-concurrency | kotlin-concurrency | parallel-executions | cooperative-multitasking | non-preemptive-multitasking

TLDRCoroutines are a mechanism that runs a set of instructions in a cooperative way. This means that the operating system doesn't control the scheduling of tasks or processes performed by coroutines. This is useful for operations where we need to wait for certain operations to complete like a query to a database or a **REST** request to the website.via the TL;DR App

If you are a bit familiar with coroutines, you probably already have the idea that coroutines are a mechanism that runs a set of instructions. You’ve probably also heard the term cooperative multitasking or non-preemptive multitasking. In broad terms, this means that coroutines are concurrency primitives that cooperatively execute a set of instructions and that the operating system doesn't control the scheduling of tasks or processes performed by coroutines. Instead, it relies on the program, and platform that runs them to do that. As such, coroutines can yield control back to the scheduler to allow other threads to run. This is particularly useful for operations where we need to wait for certain operations—a query to a database or a REST request to the website—to be completed. Being cooperative means that while we wait, the scheduler can execute other segments of coroutines. This is what I talk about in this post with a focus on the interesting properties that coroutines have of running concurrently. If they run on a single Thread and are released at the same time, we won't be able to predict the order in which they execute.


I have a simple example of this phenomenon available on my GitHub repository implemented with Kotlin.

Here is how to use it from the command line:

git clone https://github.com/jesperancinha/jeorg-kotlin-test-drives.git
cd jeorg-kotlin-coroutines/coroutines-crums-group-1
make b

The executable class is SimpleConcurrency in Kotlin:

class SimpleConcurrency {
   companion object {
       /**
        * This test runs sleep with the purpose to show concurrency in coroutines
        */
       @JvmStatic
       fun main(args: Array<String> = emptyArray()) = runBlocking {
           val timeToRun = measureTimeMillis {
               val coroutine1 = async {
                   delay(Duration.ofMillis(nextLong(100)))
                   async {
                       val randomTime = nextLong(500)
                       sleep(Duration.ofMillis(randomTime))
                       println("Coroutine 1 is complete in $randomTime!")
                   }.await()
               }
               val coroutine2 =
                   async {
                       delay(Duration.ofMillis(nextLong(100)))
                       async {
                           val randomTime = nextLong(500)
                           sleep(Duration.ofMillis(randomTime))
                           println("Coroutine 2 is complete in $randomTime!")
                       }.await()
                   }
               coroutine2.await()
               coroutine1.await()
           }
           println("Time to run is $timeToRun milliseconds")
       }
   }
}

If you run this class multiple times, the result will be different:

Coroutine 2 is complete in 342!
Coroutine 1 is complete in 389!
Time to run is 805 milliseconds!

or

Coroutine 1 is complete in 291!
Coroutine 2 is complete in 140!
Time to run is 516 milliseconds

The code looks complicated but although this seems an easy problem to exemplify, working with coroutines can be quite challenging as when we look at the details it starts to look very confusing.

For example, how would I be able to test the random start of the coroutines? To start off, I need a blocking scope because I want the coroutines to run in a single Thread and I achieve that with runBlocking. Then I want to make sure they start at random times. However, the control may be random, if I would just start coroutines using a sequential and imperative type of code, the second asynchronous declared coroutine would always start later on than the first. This is why I first launch two coroutines with async and then randomly delay them. This way I make sure that they will start at random times just like a real case. For both of these, I then start a sub-coroutine, where I, instead of using delay, use the blocking sleep function. This is to ensure that the first coroutine that sleeps will then block the other. If we observe our result, we do see that they start in random order and that the sum of their execution times matches the total execution time of the program proving that they concurrently try to gain control of a Thread. In real cases, we do not use runBlocking and there are multiple threads involved to share with these concurrency primitives called coroutines. This is here only to exemplify that.


More on this in this video:

https://youtube.com/shorts/JdleGvk8Xj8?feature=share&embedable=true

Also published here.


Written by jesperancinha | Software Engineer for 10+ Years, OCP11, Spring Professional 2020 and a Kong Champion
Published by HackerNoon on 2024/03/18