跪拜 Guibai
← All articles
Android

Blocking vs Suspension: What Actually Happens When a Kotlin Coroutine Waits

By 潜龙勿用之化骨龙 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

The difference between blocking and suspension directly affects Android responsiveness: a suspended coroutine keeps the main thread available for UI updates, while a blocked thread can cause jank or ANRs. The article clarifies common misconceptions that hurt production code.

Summary

The classic even-odd printing problem is used to compare Java threads and Kotlin coroutines. Both versions rely on semaphores to hand off execution, but the waiting behavior diverges: in the thread version, `Semaphore.acquire()` and `Thread.sleep()` block the worker thread, making it unavailable during the wait. The coroutine version uses `delay()` and the coroutine `Semaphore`, which suspend the coroutine without blocking the underlying thread.

Log output shows all coroutines run on the same `main` thread because no dispatcher is specified; coroutines inherit the `runBlocking` context. While one coroutine is suspended, the same thread runs a background coroutine that logs periodically, proving that suspension does not equal thread switching. A follow-up note warns that `Thread.sleep()` inside a coroutine still blocks the thread, so Main-safe APIs remain essential in Android.

Takeaways
— In the thread version, `Semaphore.acquire()` and `Thread.sleep()` block the worker thread, leaving it idle and unable to run other tasks.
— In the coroutine version, `Semaphore.acquire()` and `delay()` suspend the coroutine, freeing the underlying thread to execute other coroutines.
— All coroutines in the example run on the `main` thread, so suspension does not imply a thread switch.
— `Semaphore(1, acquiredPermits = 1)` creates a coroutine semaphore with one permit already consumed, equivalent to Java's `Semaphore(0)`.
— Coroutines do not eliminate waiting or reduce task duration; they only avoid tying up a thread while waiting.
— Calling `Thread.sleep()` inside a coroutine still blocks the underlying thread; only `delay()` suspends without blocking.
— The article suggests trying alternatives like `synchronized`+`wait/notify`, `ReentrantLock`+`Condition`, `Mutex`, `Channel`, or actor models to internalize blocking vs suspension.
Conclusions

The example demonstrates that coroutines on a single thread can interleave work without any thread switching, which counters the common belief that lightweight threads always map to different threads.

The distinction between communication mechanism and what gets suspended is a useful lens for evaluating any concurrency tool: knowing whether a wait blocks a thread or suspends a coroutine determines its suitability for UI-heavy workloads.

The warning about `Thread.sleep()` inside coroutines is a practical reminder that the coroutine framework does not magic away blocking calls; developers must verify Main-safety at the API level.

Concepts & terms
Main-safe
An API is Main-safe if calling it from the main thread never blocks it for a significant time. Coroutines alone do not make operations non-blocking; the underlying functions must be written or chosen with this property in mind.
Non-blocking suspension
A coroutine can pause its own execution (suspend) without blocking the thread it runs on, allowing the thread to execute other coroutines. This is the key mechanism behind Kotlin's efficient concurrent workloads.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗