Kotlin Coroutines : Suspending vs Blocking
Originally posted on : yveskalume.dev
Coroutines are a concurrency design pattern used in programming to write asynchronous code in a more sequential and readable manner. Both suspending and blocking play a role in managing the flow of asynchronous code.
Suspending
In coroutine terminology, Suspending functions are functions that can suspend a coroutine. When a coroutine encounters a suspending function, it can voluntarily suspend its execution without blocking the underlying thread. This allows other coroutines to continue running in the meantime. Suspending functions are marked with the suspend
keyword.
The suspension doesn’t necessarily mean the entire thread is blocked; it simply allows the coroutine to release the thread and let other tasks run.
Blocking
“Blocking” refers to the act of stopping the execution of a thread until a certain operation completes. When a thread is blocked, it is essentially waiting for some resource or operation to finish, and it cannot do anything else during that time.
In contrast to suspending, blocking operations can be less efficient in terms of resource utilization because a blocked thread is not available to perform other tasks.
Traditional synchronous code often involves blocking operations, where the program waits for an I/O operation, database query, or other time-consuming tasks to complete before moving on.
An example with runBlocking and coroutineScope
runBlocking
and coroutineScope
builders may look similar because they both wait for their body and all its children to complete. The main difference is that the runBlocking
method blocks the current thread for waiting, while coroutineScope
just suspends, releasing the underlying thread for other usages. Because of that difference, runBlocking
is a regular function and coroutineScope
is a suspending function.
coroutineScope
The coroutines initiated through coroutineScope
exhibit suspendable behavior. To illustrate this concept, we'll initiate a coroutine dispatcher using a fixed thread pool with two threads as our execution context:
val dispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
Now, let’s create a function that launches ten coroutines. Each coroutine, in turn, initiates a child coroutine using coroutineScope
:
fun exampleCoroutineScope() = runBlocking {
(1..10).forEach {
launch(dispatcher) {
coroutineScope {
println("Coroutine $it started on thread: ${Thread.currentThread().name}")
delay(500)
println("Coroutine $it completed on thread: ${Thread.currentThread().name}")
}
}
}
}
Here, we encapsulate our logic within runBlocking
to seamlessly integrate with our blocking code. Within this block, we utilize launch
to dispatch suspendable coroutines to any inactive thread in the context's thread pool.
Additionally, coroutineScope
is employed to initiate a coroutine that invokes delay()
with a 500-millisecond duration. The delay()
function is a suspension point for the coroutine launched by coroutineScope
.
Let’s execute this function to observe Kotlin’s ability to suspend these coroutines :
fun main() {
val timeInMills = measureTimeMillis {
exampleCoroutineScope()
}
println("Time : $timeInMills")
}
Here is the output :
Coroutine 1 started on thread: pool-1-thread-1
Coroutine 2 started on thread: pool-1-thread-2
Coroutine 3 started on thread: pool-1-thread-1
Coroutine 4 started on thread: pool-1-thread-1
Coroutine 5 started on thread: pool-1-thread-1
Coroutine 6 started on thread: pool-1-thread-1
Coroutine 7 started on thread: pool-1-thread-1
Coroutine 8 started on thread: pool-1-thread-1
Coroutine 9 started on thread: pool-1-thread-1
Coroutine 10 started on thread: pool-1-thread-1
Coroutine 2 completed on thread: pool-1-thread-1
Coroutine 1 completed on thread: pool-1-thread-2
Coroutine 3 completed on thread: pool-1-thread-1
Coroutine 4 completed on thread: pool-1-thread-2
Coroutine 5 completed on thread: pool-1-thread-1
Coroutine 6 completed on thread: pool-1-thread-2
Coroutine 7 completed on thread: pool-1-thread-2
Coroutine 8 completed on thread: pool-1-thread-1
Coroutine 9 completed on thread: pool-1-thread-1
Coroutine 10 completed on thread: pool-1-thread-2
Time : 633
The suspension mechanism ensures that a suspended coroutine does not hinder any threads, allowing another coroutine to seamlessly take advantage of the thread and each coroutine can efficiently resume its execution on any available thread within the pool.
runBlocking
fun exampleRunBlocking() = runBlocking {
(1..10).forEach {
launch(context) {
runBlocking {
println("runBlocking $it started on ${Thread.currentThread().name}")
delay(500)
println("runBlocking $it ended on ${Thread.currentThread().name}")
}
}
}
}
Output :
runBlocking 1 started on pool-1-thread-1
runBlocking 2 started on pool-1-thread-2
runBlocking 1 ended on pool-1-thread-1
runBlocking 2 ended on pool-1-thread-2
runBlocking 3 started on pool-1-thread-2
runBlocking 4 started on pool-1-thread-1
runBlocking 3 ended on pool-1-thread-2
runBlocking 4 ended on pool-1-thread-1
runBlocking 5 started on pool-1-thread-2
runBlocking 6 started on pool-1-thread-1
runBlocking 6 ended on pool-1-thread-1
runBlocking 5 ended on pool-1-thread-2
runBlocking 7 started on pool-1-thread-1
runBlocking 8 started on pool-1-thread-2
runBlocking 7 ended on pool-1-thread-1
runBlocking 8 ended on pool-1-thread-2
runBlocking 9 started on pool-1-thread-1
runBlocking 10 started on pool-1-thread-2
runBlocking 9 ended on pool-1-thread-1
runBlocking 10 ended on pool-1-thread-2
Time : 2597
In this scenario, it’s observed that each coroutine initiated by runBlocking
completed its execution on the same thread where it started.
This behavior is indicative of the fact that the coroutine launched within runBlocking
did not respond to the suspension point created by the delay()
call. They are not suspendable.
Conclusion
In summary, the key difference lies in how coroutines handle waiting for asynchronous operations. Suspending allows a coroutine to yield control without blocking the underlying thread, enabling better concurrency and resource utilization. Blocking, on the other hand, involves the thread waiting until the operation completes, which can lead to inefficiencies in terms of resource usage. Coroutines are designed to be more efficient by leveraging suspending functions to avoid unnecessary blocking and allow other tasks to proceed in the meantime.