Coroutines

Kotlin's documentation often refers to coroutines as lightweight threads. This is mostly because, like threads, coroutines define the execution of a set of instructions for a processor to execute. Also, coroutines have a similar life cycle to that of threads.

A coroutine is executed inside a thread. One thread can have many coroutines inside it, but as already mentioned, only one instruction can be executed in a thread at a given time. This means that if you have ten coroutines in the same thread, only one of them will be running at a given point in time.

The biggest difference between threads and coroutines, though, is that coroutines are fast and cheap to create. Spawning thousands of coroutines can be easily done, it is faster and requires fewer resources than spawning thousands of threads.

Take this code as an example. Don't worry about the parts of the code you don't understand yet:

suspend fun createCoroutines(amount: Int) {
val jobs = ArrayList<Job>()
for (i in 1..amount) {
jobs += launch {
delay(1000)
}
}
jobs.forEach {
it.join()
}
}

This function creates as many coroutines as specified in the parameter amount, delays each one for a second, and waits for all of them to end before returning. This function can be called, for example, with 10,000 as the amount of coroutines:

fun main(args: Array<String>) = runBlocking {
val time = measureTimeMillis {
createCoroutines(10_000)
}

println("Took $time ms")
}
measureTimeMillis() is an inline function that takes a block of code and returns how long its execution took in milliseconds. measureTimeMillis() has a sibling function, measureNanoTime(), which returns the time in nanoseconds. Both functions are quite practical when you want a rough estimate of the execution time of a piece of code.

In a test environment, running it with an amount of 10,000 took around 1,160 ms, while running it with 100,000 took 1,649 ms. The increase in execution time is so small because Kotlin will use a pool of threads with a fixed size, and distribute the coroutines among those threads – so adding thousands of coroutines will have little impact. And while a coroutine is suspended – in this case because of the call to delay() – the thread it was running in will be used to execute another coroutine, one that is ready to be started or resumed.

How many threads are active can be determined by calling the activeCount() method of the Thread class. For example, let's update the main() function to do so:

fun main(args: Array<String>) = runBlocking {
println("${Thread.activeCount()} threads active at the start")
val time = measureTimeMillis {
createCoroutines(10_000)
}
println("${Thread.activeCount()} threads active at the end")
println("Took $time ms")
}

In the same test environment as before, it was found that in order to create 10,000 coroutines, only four threads needed to be created:

But once the value of the amount being sent to createCoroutines() is lowered to one, for example, only two threads are created:

Notice how at the start the application, already had two threads. This is because of a thread called Monitor Control+Break, which is created when you run an application in IntelliJ IDEA. This thread is in charge of processing the hotkey Control+Break, which dumps the information of all the threads running. If you run this code from the command line, or in IntelliJ using debug mode, it will display just one thread at the start and five at the end.

It's important to understand that even though a coroutine is executed inside a thread, it's not bound to it. As a matter of fact, it's possible to execute part of a coroutine in a thread, suspend the execution, and later continue in a different thread. In our previous example this is happening already, because Kotlin will move coroutines to threads that are available to execute them. For example, by passing 3 as the amount to createCoroutines(), and updating the content of the launch() block  so that it prints the current thread, we can see this in action:

suspend fun createCoroutines(amount: Int) {
val jobs = ArrayList<Job>()
for (i in 1..amount) {
jobs += launch {
println("Started $i in ${Thread.currentThread().name}")
delay(1000)
println("Finished $i in ${Thread.currentThread().name}")
}
}
jobs.forEach {
it.join()
}
}

You will find that in many cases they are being resumed in a different thread:

A thread can only execute one coroutine at a time, so the framework is in charge of moving coroutines between threads as necessary. As will be explained in detail later, Kotlin is flexible enough to allow the developer to specify which thread to execute a coroutine on, and whether or not to confine the coroutine to that thread.

Chapter 4Suspending Functions and the Coroutine Context, explains how to resume a coroutine in a thread different than the one in which it was started, while Chapter 7Thread Confinement, Actors, and Mutexes, covers thread confinement in detail.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.218.221