Why You Should Use runBlocking With Caution
Why You Should Use runBlocking With Caution
runBlocking is a coroutine builder in Kotlin that bridges regular blocking code and suspend functions. It blocks the current thread until the coroutine execution is complete. While it may seem convenient for calling suspend functions from non suspend contexts, its use in Android UI code should be approached with caution due to its blocking nature. It is a synchronization mechanism rather than an asynchronous or non blocking solution. By the end of this lesson, you will be able to:
- Explain the internal mechanism of
runBlockingand why it blocks the calling thread. - Identify why calling
runBlockingon the Android main thread causes ANR. - Describe valid use cases for
runBlockingin testing and background thread execution. - Recognize common mistakes like nesting
runBlockingand using it inside suspend functions. - Compare
runBlockingwithrunTestfor coroutine testing. - Explain why
job.join()is often a better alternative in coroutine contexts.
How runBlocking Works Internally
runBlocking is fundamentally different from other coroutine builders like launch and async. While those builders return immediately and let the coroutine run asynchronously, runBlocking holds the calling thread hostage until the coroutine finishes.
The internal implementation captures the current thread using Thread.currentThread() and creates a BlockingCoroutine that parks the thread in a loop until the coroutine completes:
val currentThread = Thread.currentThread()
val coroutine = BlockingCoroutine<T>(
newContext, currentThread, eventLoop
)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
Inside joinBlocking(), a while(true) loop processes events from an internal event loop and parks the thread between iterations. The thread remains blocked until isCompleted returns true:
fun joinBlocking(): T {
while (true) {
val parkNanos =
eventLoop?.processNextEvent() ?: Long.MAX_VALUE
if (isCompleted) break
parkNanos(this, parkNanos)
}
val state = this.state.unboxState()
(state as? CompletedExceptionally)?.let { throw it.cause }
return state as T
}
This design makes runBlocking inherently synchronous and thread blocking. The thread it is invoked on cannot do any other work until the coroutine finishes. The event loop inside joinBlocking() does process coroutine continuations dispatched to the blocked thread, which prevents simple deadlocks within the same runBlocking scope, but it does not process Android message queue events.
Why It Is Dangerous on the Main Thread
On Android, the main thread handles all UI rendering, input events, and lifecycle callbacks. Blocking it with runBlocking prevents the system from processing any of these, leading to frozen UI and eventually an Application Not Responding (ANR) dialog if the block takes more than a few seconds.
Because runBlocking determines the blocking thread using Thread.currentThread(), calling it from the main thread will always block the main thread regardless of which dispatcher the coroutine body uses internally. Even if the coroutine body runs entirely on Dispatchers.IO, the main thread remains parked in the joinBlocking() loop, unable to process any UI events, lifecycle callbacks, or Choreographer frame requests until the coroutine completes.
This interview continues for subscribers
Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.
Become a Sponsor