In this unique blog, we delve into the world of Kotlin coroutines through a series of output questions. Each question presents a distinct scenario, allowing us to understand and reinforce our knowledge of coroutine concepts. By exploring and analyzing the outputs, we gain a deeper understanding of the behavior and intricacies of Kotlin coroutines. Join me on this journey as we unravel the power of coroutines in Kotlin and enhance our proficiency in concurrent programming.
I’ll be using the following two suspend functions in the questions
suspend fun doLongRunningTaskOne(): Int { delay(2400) println("Task one is getting executed") return 33 } suspend fun doLongRunningTaskTwo(): Int { delay(1500) println("Task two is getting executed") return 6 }
Let’s jump right in now:
Question 1
runBlocking { doLongRunningTaskOne() doLongRunningTaskTwo() println("Fired both tasks") } println("Completed my execution")
Output
Task one is getting executed Task two is getting executed Fired both tasks Completed my execution
Explanation
- runBlocking gives us a coroutine scope on main thread
- It is a blocking call which means until the code inside runBlocking is completed, the next statements will not be executed
- By default, the code inside a coroutine is sequential
- So first the task one gets completed, then the second task is called, and when that gets completed, the last print statement is executed
Question 2
runBlocking { launch { doLongRunningTaskOne() } launch { doLongRunningTaskTwo() } println("I've launched both coroutines") } println("Completed my execution")
Output
I've launched both coroutines Task two is getting executed Task one is getting executed Completed my execution
Explanation
- launch is fire and forget, it doesn’t care about the result returned by the tasks. So both the child coroutines (launch) start executing on different threads and don’t block our main thread
- Since they will take some time to run, the first statement printed is “I’ve launched both coroutines”
- Task two is executed faster as it has shorted delay in it that’s why the next statement printed is from task 2
- Then the print statement in task one is printed
- Lastly, since runBlocking is a blocking call, no code is executed outside it until it’s execution is completed. So once it is completed, the last “Completed my execution” statement is printed
Question 3
runBlocking { coroutineScope { launch { doLongRunningTaskOne() } launch { doLongRunningTaskTwo() } println("I've launched both coroutines") } println("I'm outside coroutineScope") } println("Completed my execution")
Output
I've launched both coroutines Task two is getting executed Task one is getting executed I'm outside coroutineScope Completed my execution
Explanation
- What’s changed here is that we have added a coroutineScope inside runBlocking and both the launch methods are called inside it
- coroutineScope is a suspending function so the “I’m outside coroutineScope” statement will be printed after the coroutineScope has completed it’s execution
Question 4
runBlocking { coroutineScope { launch { doLongRunningTaskOne() }.join() launch { doLongRunningTaskTwo() }.join() println("I've launched both coroutines") } println("I'm outside coroutineScope") } println("Completed my execution")
Output
Task one is getting executed Task two is getting executed I've launched both coroutines I'm outside coroutineScope Completed my execution
Explanation
- Here we have added join() on the two launch functions
- join is a suspending function, it waits for the coroutine to complete before moving to the next statement
- That is why the task one statement is printed first this time
- This code will take more time to execute because we have made the calls sequential here
Question 5
runBlocking { CoroutineScope(Dispatchers.IO).launch { launch { doLongRunningTaskOne() } launch { doLongRunningTaskTwo() } println("I've launched both coroutines") } println("I'm outside coroutineScope") } println("Completed my execution")
Output
I'm outside coroutineScope Completed my execution
Explanation
- Whooo!!! What happened here??
- None of the code inside the custom coroutine scope got executed
- The runBlocking scope finished even before the CoroutineScope could get finished
- This is because the runBlocking function is intended to block the current thread until it’s scope completed.
- However, any nested coroutines scopes launched within it will not be awaited unless explicitly done so
Question 6
runBlocking { CoroutineScope(Dispatchers.IO).launch { launch { doLongRunningTaskOne() } launch { doLongRunningTaskTwo() } println("I've launched both coroutines") }.join() println("I'm outside coroutineScope") } println("Completed my execution")
Output
I've launched both coroutines Task two is getting executed Task one is getting executed I'm outside coroutineScope Completed my execution
Explanation
- This time the block of code inside the custom Coroutine scope gets executed because we added join to add
- Join is a suspend function which asks the system to wait until it’s execution is completed
Question 7
runBlocking { CoroutineScope(Dispatchers.IO).launch { launch { doLongRunningTaskOne() } launch { doLongRunningTaskTwo() } println("I've launched both coroutines of scope 1") CoroutineScope(Dispatchers.IO).launch { launch { doLongRunningTaskOne() } launch { doLongRunningTaskTwo() } println("I've launched both coroutines of scope 2") } }.join() println("I'm outside coroutineScope") } println("Completed my execution")
Output
I've launched both coroutines of scope 1 I've launched both coroutines of scope 2 Task two is getting executed Task two is getting executed Task one is getting executed Task one is getting executed I'm outside coroutineScope Completed my execution
Explanation
- Here we didn’t add join to the second nested coroutine scope but still it got executed
Question 8
runBlocking { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("Final answer is ${one.await() + two.await()}") } println("Completed my execution")
Output
Task two is getting executed Task one is getting executed Final answer is 39 Completed my execution
Explanation
- Here we are using the result returned by our two suspend functions
- Both the suspend functions are called parallelly
- await is a suspend functions which blocks the main thread. It gets the result from the Deferred object
Question 9
runBlocking { val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() } val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() } println("Final answer is ${one.await() + two.await()}") } println("Completed my execution")
Output
Task one is getting executed Task two is getting executed Final answer is 39 Completed my execution
Explanation
- This code is executed sequentially because we used Lazy start for our coroutines
- What Lazy does is it starts the coroutine only when it’s result is required
- So when we write our print statement, we called one.await first so the system waits until it gets the result for the first task which would be after around 2400 ms
- Once it gets result of first task, then it starts second task and get’s it’s result after another 1500 ms
- So this block of code will take more time as compared to our last code(Question 6) because of sequential calls
Job Offers
Question 10
runBlocking { val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() } val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() } one.start() two.start() println("Final answer is ${one.await() + two.await()}") } println("Completed my execution")
Output
Task two is getting executed Task one is getting executed Final answer is 39 Completed my execution
Explanation
- This time the output is same as Question 6 because even though we used lazy start, we started the coroutines parallelly before using them in the same print statement
- The time taken to execute this block of code will also be same as the one in Question 6
Question 11
runBlocking { launch { repeat(5){ println("Iteration $it in Coroutine 1") } } launch { repeat(5){ println("Iteration $it in Coroutine 2") } } }
Output
Iteration 0 in Coroutine 1 Iteration 1 in Coroutine 1 Iteration 2 in Coroutine 1 Iteration 3 in Coroutine 1 Iteration 4 in Coroutine 1 Iteration 0 in Coroutine 2 Iteration 1 in Coroutine 2 Iteration 2 in Coroutine 2 Iteration 3 in Coroutine 2 Iteration 4 in Coroutine 2
Explanation
- Even though both launch coroutines are running in parallel, the first coroutine completes instantaneously since there is no much computation involved.
Question 12
runBlocking { launch { repeat(5){ println("Iteration $it in Coroutine 1") yield() } } launch { repeat(5){ println("Iteration $it in Coroutine 2") } } }
Output
Iteration 0 in Coroutine 1 Iteration 0 in Coroutine 2 Iteration 1 in Coroutine 2 Iteration 2 in Coroutine 2 Iteration 3 in Coroutine 2 Iteration 4 in Coroutine 2 Iteration 1 in Coroutine 1 Iteration 2 in Coroutine 1 Iteration 3 in Coroutine 1 Iteration 4 in Coroutine 1
Explanation
- yield is a suspending function which tells the coroutine scheduler that this coroutine wants to pause it’s execution and allow other coroutines to use this thread
- So what happens here is, after the first iteration in the first coroutine, it encounters yield, asking the scheduler to pause it’s execution
- Scheduler looks if there are other coroutines which need to be executed
- When it finds one it starts executing the second coroutine and completes it
- After the second coroutine is completed, it again jumps back to the first coroutine and resume it’s execution
Question 13
runBlocking { launch { repeat(5){ println("Iteration $it in Coroutine 1") yield() } } launch { repeat(5){ println("Iteration $it in Coroutine 2") yield() } } }
Output
Iteration 0 in Coroutine 1 Iteration 0 in Coroutine 2 Iteration 1 in Coroutine 1 Iteration 1 in Coroutine 2 Iteration 2 in Coroutine 1 Iteration 2 in Coroutine 2 Iteration 3 in Coroutine 1 Iteration 3 in Coroutine 2 Iteration 4 in Coroutine 1 Iteration 4 in Coroutine 2
Explanation
- Here yield is there is both the coroutines
- After the first iteration in Coroutine 1, it looks if there are other coroutines waiting to be executed
- It finds one and starts executing Coroutine 2
- After the first iteration of Coroutine 2, it again finds yield, which means pause coroutine 2
- So it again looks for waiting coroutines, and it finds that Coroutine 1 is paused and waiting to be resumed
- So it resumes Coroutine 1, where again after second iteration it finds yield and is again paused
- This process continues until both the coroutines are completed
Question 14
runBlocking { launch { repeat(5){ println("Iteration $it in Coroutine 1") yield() } } }
Output
Iteration 0 in Coroutine 1 Iteration 1 in Coroutine 1 Iteration 2 in Coroutine 1 Iteration 3 in Coroutine 1 Iteration 4 in Coroutine 1
Explanation
- Since there is one coroutine, yield has no affect on the output
- It executes all the iteration and completes the coroutine in a single go
Question 15
runBlocking { val job = launch { repeat(1000) { doLongRunningTaskTwo() } } delay(4000L) println("I'm tired of waiting!") job.cancel() job.join() println("Now I can quit.") }
Output
Task two is getting executed Task two is getting executed I'm tired of waiting! Now I can quit.
Explanation
- The task two takes 1500 ms to get executed
- And we have cancelled our job after 4000 ms that’s why task two got executed only twice as 2 iterations took 3000 ms
- Third iteration would have returned at 4500 ms before which we cancelled our job
Question 16
runBlocking { var sum = 0 val job = launch(Dispatchers.Default) { for (i in 1..1000) { sum += i println("Partial sum after $i iterations: $sum") } } delay(5) println("I'm tired of waiting!") job.cancelAndJoin() println("Now I can quit.") }
Output
Partial sum after 1 iterations: 1 Partial sum after 2 iterations: 3 . . . Partial sum after 999 iterations: 499500 Partial sum after 1000 iterations: 500500 I'm tired of waiting! Now I can quit
Explanation
- What happened here???
- We cancelled the job only after 5 ms and still the coroutine didn’t get cancelled
- It executed all the iterations and a 1000 sum iterations could not have completed in 5 ms
- This is because the all the suspend functions inside a coroutine are cancellable. They check for cancellation of coroutine and throw CancellationException when cancelled.
- However, if a coroutine is working on a computation, it will not check for cancellation, hence can not be cancelled.
Question 17
runBlocking { var sum = 0 val job = launch(Dispatchers.Default) { for (i in 1..1000) { sum += i println("Partial sum after $i iterations: $sum") delay(500) } } println("I'm tired of waiting!") job.cancelAndJoin() println("Now I can quit.") }
Output
I'm tired of waiting! Partial sum after 1 iterations: 1 Now I can quit.
Explanation
- In this case, there is a delay after each iteration which is a suspend function and suspend functions checks for cancellations
- So, before the loop could go over to the second iteration, the launch coroutine is cancelled
- That is why only iteration of the loop get’s executed
- Also, notice that in this case “I’m tired of waiting!” is printed before even the first iteration where as in the last question, it was printed after the 1000 iterations of the loop
- If here also we add a delay before the “I’m tired of waiting!” print statement, it would have been printed after the loop iterations
Question 18
runBlocking { var sum = 0 val job = launch(Dispatchers.Default) { for (i in 1..1000) { yield() sum += i println("Partial sum after $i iterations: $sum") } } println("I'm tired of waiting!") job.cancelAndJoin() println("Now I can quit.") }
Output
I'm tired of waiting! Partial sum after 1 iterations: 1 Partial sum after 2 iterations: 3 . . . Partial sum after 49 iterations: 1225 Partial sum after 50 iterations: 1275 Now I can quit.
Explanation
- Here also, the code got cancelled before completing the 1000 iterations
- This is because yield is also a suspend function so this also allows us to cancel computable code
- Though this is not the primary purpose of yield but it does our job
Question 19
runBlocking { var sum = 0 val job = launch(Dispatchers.Default) { for (i in 1..1000) { if (isActive) { sum += i println("Partial sum after $i iterations: $sum") } } } println("I'm tired of waiting!") job.cancelAndJoin() println("Now I can quit.") }
Output
I'm tired of waiting! Partial sum after 1 iterations: 1 Partial sum after 2 iterations: 3 . . . Partial sum after 246 iterations: 30381 Partial sum after 247 iterations: 30628 Now I can quit.
Explanation
- This is the primary way we can check for cancellations in a coroutine which allows computation
- isActive checks if the coroutine is active or has been cancelled
- If we find the cancellation of the coroutine has been requested then we don’t execute the next iterations, hence achieving cancellation in a computable code
That’s it for this article. Hope it was helpful! If you like it, please hit like.
Other articles of this series:
Coroutine Cancellations and Timeouts
This article is previously published on proandroiddev.com.