Kotlin Coroutines : Suspending vs Blocking

Kotlin Coroutines : Suspending vs Blocking

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.