How does concurrency play a role in coroutines?
If you are a bit familiar with coroutines, you probably have an idea already that corouties are a mechanism that runs a set of instructions. Your probably also heard this term cooperative multitasking or non-preemptive multitasking. In broad terms, what this means is that coroutines are concurrency primitives that execute a set of instructions in a cooperative way. Meaning that the operating system doesn't control the scheduling of tasks or processes performed by coroutines. Instead, it reiies on the program, platform that run them to do that. This means that coroutines can yield control back to the scheduler to allow other threads to run and this is useful for operations where we need to wait for certain operations to complete like for example a query to a database or a REST request to the website. Being cooperative, means that while we wait, the scheduler can use that period to execute other segments of coroutines. This is what I talk about in this video. The phenomenon I'm mostly discussing is the interesting property that coroutines have of running concurrently. So if they run on a single Thread and they are released at the same time, we won't be able to predict the order to 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:
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 compicated 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 of 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 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 that the first. This is why I first launch two coroutines with async
and then randomly delay them. This way I a 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, I now 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.
The video can be found here:
Top comments (0)