Let us write a program that prints a list of 5 even and 5 odd numbers parallelly. We’ll need to launch two coroutines, one for each list.
fun main() {
runBlocking {
// Coroutine 1 : Even number generator
launch {
println("C1 Executing on ${Thread.currentThread().name}")
repeat(5) {
println("Even : ${it * 2}")
}
}
// Coroutine 2 : Odd number generator
launch {
println("C2 Executing on ${Thread.currentThread().name}")
repeat(5) {
println("Odd : ${2 * it + 1}")
}
}
}
}
Because of parallel execution, you might expect the output to be a combination of even and odd lines rather than even and then odd lines :
C1 Executing on main
Even : 0
C2 Executing on main
Odd : 1
Even : 2
Odd : 3
Even : 4
Odd : 5
Even : 6
Odd : 7
Even : 8
Odd : 9
But in reality, the output indicates sequential execution i.e. even and then odd lines :
C1 Executing on main
Even : 0
Even : 2
Even : 4
Even : 6
Even : 8
C2 Executing on main
Odd : 1
Odd : 3
Odd : 5
Odd : 7
Odd : 9
Let’s understand the reason behind this.
Firstly, both coroutines share the same thread - Main
because Dispatcher is not specified and runBlocking()
uses Main
as its default dispatcher. To verify this, we have used Thread.currentThread().name
to get the name of the current thread :
println("C1 Executing on ${Thread.currentThread().name}")
As discussed earlier, Main
dispatcher provides only a single thread for execution so both coroutines are scheduled on the same thread.
Coroutine 1 is scheduled first because it is written before Coroutine 2. Coroutine 2 will be scheduled only when Coroutine 1 suspends (pauses) or finishes. Turns out, Coroutine 1 does not suspend rather it continously prints 5 lines and then finishes. Thus Coroutine 2 is scheduled only after Coroutine 1 finishes. Hence the sequential execution.
For parallel execution, we need multiple threads. Default
& IO
dispatcher provide multiple threads for execution. So let’s change runBlocking
dispatcher to Default
:
fun main() {
runBlocking(Dispatchers.Default) {
// Coroutine 1 (C1) : Even number generator
launch {
repeat(5) {
println("Even : ${it * 2}")
}
}
// Coroutine 2 (C2) : Odd number generator
launch {
repeat(5) {
println("Odd : ${2 * it + 1}")
}
}
}
}
Although Default dispatcher is used, both coroutines still share the same thread DefaultDispatcher-worker-1
and output is still the same :
Executing on DefaultDispatcher-worker-1
Even : 0
Even : 2
Even : 4
Even : 6
Even : 8
Executing on DefaultDispatcher-worker-1
Odd : 1
Odd : 3
Odd : 5
Odd : 7
Odd : 9
Let’s understand the reason behind this as well.
Printing 5 lines is a very small and extremely fast task for computer as they perform million operations per second. C1 is dispatched immediately on a Default thread. By the time C2 is scheduled, C1 is already finished. As a result of which, the same thread is freed and can be used by C2. Hence, sequential operation is performed.
To be able to witness parallel execution, we have to increase the number of operations (say 500) :
fun main() {
runBlocking(Dispatchers.Default) {
// Coroutine 1 (C1) : Even number generator
launch {
println("Executing on ${Thread.currentThread().name}")
repeat(500) {
println("Even : ${it * 2}")
}
}
// Coroutine 2 (C2) : Odd number generator
launch {
println("Executing on ${Thread.currentThread().name}")
repeat(500) {
println("Odd : ${2 * it + 1}")
}
}
}
}
Now that number of operations is increased, you can see both coroutines being dispatched on different threads and hence parallel execution :
C1 Executing on DefaultDispatcher-worker-2
Even : 0
Even : 2
Even : 4
...
Even : 764
C2 Executing on DefaultDispatcher-worker-3
Even : 766
Odd : 1
Even : 768
Odd : 3
Even : 770
Odd : 5
Even : 772
Odd : 7
Even : 774
Odd : 9
Odd : 11
Odd : 13
...
Odd : 117
Even : 780
...
Even : 990
Odd : 119
...
Note
- C1 is scheduled on the thread C1 Executing on
DefaultDispatcher-worker-2
while C2 on C1 Executing onDefaultDispatcher-worker-3
- The interleaving of C1 and C2 is not perfect because threads switching is upto CPU. The order of statements is random and may change on each execution.