Explain precisely what 'blocking the current thread' means for runBlocking, including its event loop and child coroutines.
answer
- Caller thread parked until everything completes
- Default = event loop on the current thread
- Children must finish first (structured concurrency)
- Passing a dispatcher still blocks the original thread
- Exceptions rethrown to the caller
basics
~10 sThe thread that calls runBlocking stays busy and cannot do other work until the coroutine finishes. Inside, runBlocking runs an event loop on that thread that drives the coroutines until they all complete.
solid answer
~50 sWhen you call runBlocking, the calling thread is dedicated to it: that thread does not return to its caller until the block and all child coroutines complete. By default runBlocking installs an **event loop** on the current thread (its dispatcher is an internal `BlockingEventLoop`/event-loop dispatcher), so suspended continuations that target this context are resumed on the same thread, and `delay` is honored via that loop. This is why a coroutine inside runBlocking that has nothing else to dispatch to will still run on the caller's thread. runBlocking obeys **structured concurrency**: it waits for every coroutine `launch`ed/`async`ed inside its scope before returning. If you pass a different dispatcher (e.g. `runBlocking(Dispatchers.IO)`), the block runs on that dispatcher, but the original thread is still blocked, parked, waiting for completion. Exceptions thrown in the block (or uncaught in children) are rethrown to the caller.
code
kotlin · 10 linesimport kotlinx.coroutines.*
fun main() {
val t0 = System.currentTimeMillis()
runBlocking {
launch { delay(300); println("A") }
launch { delay(100); println("B") }
} // returns only after BOTH children finish
println("elapsed ~300ms: " + (System.currentTimeMillis() - t0))
}go deeper
Understands the calling thread waits until the work finishes.
Explains the event loop on the current thread and that children must complete before it returns.
Distinguishes 'thread blocked to caller' from 'thread busy running the loop', and how passing a dispatcher changes execution but not the caller block.
Reasons about interaction with bounded thread pools and interruption/cancellation when runBlocking sits at a service boundary.
## 'Blocking the current thread' — what actually happens A **thread** is an OS-scheduled execution path. **Blocking** it means it is parked/occupied and cannot execute anything else until released. `runBlocking` *blocks the thread that invoked it*. That call does not return control to its caller until the coroutine it started — and all of that coroutine's children — finish. ## The internal event loop Unlike `launch`/`async`, a coroutine has to run *somewhere*. By default `runBlocking` does not hand work to a thread pool; instead it sets up an **event loop** on the current thread (an internal event-loop dispatcher). Concretely: - Coroutines created in the block without an explicit dispatcher inherit this event-loop context, so they are **dispatched back onto the blocked thread**. - `delay` doesn't spin or sleep the OS thread away from coroutine work — it schedules a resumption in the event loop, which the loop services when the time elapses. - The thread is therefore *blocked to the outside* but *busy running the loop* internally. It cycles: run ready continuations, wait for the next scheduled one, repeat, until everything completes. ```kotlin import kotlinx.coroutines.* fun main() { println(Thread.currentThread().name) // e.g. main runBlocking { launch { println(Thread.currentThread().name) // main -> same thread via event loop delay(100) println("child done") } println("parent before child completes") } println("runBlocking returned") // only after child finished } ``` ## Structured concurrency: it waits for children The block's receiver is a `CoroutineScope`. Any `launch`/`async` started in it is a **child** of the runBlocking coroutine. runBlocking does not return until **all children complete** — that is structured concurrency. So "parent before child completes" prints, but `runBlocking` itself only returns after `child done`. ## Passing a dispatcher ```kotlin runBlocking(Dispatchers.IO) { // block runs on an IO dispatcher thread } ``` Even here, the **original calling thread is still blocked**, parked, waiting; the work merely executes on IO threads. So changing the dispatcher does not make runBlocking non-blocking for the caller. ## Exceptions and cancellation - An exception thrown directly in the block, or an uncaught exception in a `launch` child, propagates and is **rethrown** by runBlocking to the caller (after cancelling siblings). - If the blocked thread is interrupted, runBlocking responds by cancelling the coroutine. ## Keywords / APIs `runBlocking`, `CoroutineScope`, `delay`, `launch`/`async`, `Dispatchers.IO/Default`, event-loop dispatcher, structured concurrency, `Job`.
- Does runBlocking(Dispatchers.IO) stop blocking the calling thread?No. The block executes on IO threads, but the original calling thread is still parked and blocked until the block completes.
- Why does a launch inside runBlocking often print the same thread name as main?Because runBlocking installs an event-loop dispatcher on the current thread, and children inherit it, so they resume on that same thread.
The thread is a chef who locks the kitchen door (blocks) but keeps cooking every order on a to-do loop until the last dish is plated, then unlocks and leaves.
saying these in an interview costs you the question
- Saying runBlocking returns before its launch children finish
- Thinking delay inside runBlocking frees the OS thread for other JVM work
- Believing a custom dispatcher makes runBlocking non-blocking for the caller
- Not knowing it runs an event loop on the current thread by default
- Ignoring that uncaught child exceptions are rethrown to the caller