跪拜 Guibai
← All articles
Android · Kotlin

Kotlin Coroutines: '协' Means Collaboration, Not Lightweight Threads

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

Treating coroutines as lightweight threads keeps developers reaching for blocking calls on the main thread and hand-rolled coordination, which is exactly what causes ANRs and lifecycle leaks on Android. Reading them as a collaboration model changes the default answer to 'who owns this task and what happens when it fails.'

Summary

Kotlin coroutines are usually introduced with launch, async, suspend, and Dispatchers, but focusing on those APIs leads to the familiar oversimplification: coroutines as lighter threads. A better entry point is the character '协' in the Chinese word for coroutine (协程): collaboration. Threads provide execution resources, coroutines provide a suspendable task model, and structured concurrency decides how tasks coordinate.

To make the distinction concrete, a side-by-side walkthrough of Java's coordination tools shows where they stop. synchronized and Lock only solve mutual exclusion; CountDownLatch needs a manually maintained counter and says nothing about task success, failure, or ownership; Future.get() blocks a thread; BlockingQueue blocks producers and consumers while waiting. On Android, running those waits on the main thread is the direct path to ANRs.

Coroutines replace blocking waits with suspension. coroutineScope waits for child coroutines and propagates cancellation, Deferred.await() suspends instead of blocking, Mutex suspends instead of parking a thread, and Channel gives producers and consumers backpressure plus explicit closing where BlockingQueue needs a poison pill. The catch: suspension only helps when the call chain is genuinely suspendable, and coroutines do not make shared state thread-safe by themselves.

Takeaways
— `count++` is read-compute-write; without a lock, interleaved threads can lose updates.
— `synchronized` and `Lock` coordinate mutual exclusion only; they do not express task completion, ordering, or lifecycle.
— `CountDownLatch` needs a manually initialized counter, and every task must call `countDown()`; a zero count means the flow ended, not that tasks succeeded.
— `Future.get()` blocks the calling thread, and putting it inside a coroutine does not make it non-blocking.
— Main-thread waits on `get()`, `join()`, `await()`, `take()`, blocking I/O, or a long-held lock can produce ANRs.
— When a coroutine suspends, the thread it was running on is released to do other work; blocking stops the thread itself.
— `coroutineScope` waits for all child coroutines and propagates cancellation, replacing manual counters with task structure.
— `Deferred.await()` suspends instead of blocking, but it neither switches threads nor fixes blocking code inside a child task.
— `Mutex.withLock` suspends a coroutine waiting for a lock, unlike a thread blocked on `synchronized`.
— `Channel.send()` and `receive()` suspend when the channel is full or empty; capacity is the backpressure policy, and `trySend()`/`tryReceive()` avoid suspension.
— `Channel.close()` gives an explicit end signal so a `for` loop can finish; a raw channel still needs scope design to avoid leaks.
— Threads are execution resources, coroutines are suspendable tasks, and structured concurrency defines task collaboration and lifetime.
Conclusions

The CountDownLatch-to-coroutineScope comparison is deliberately not a direct replacement: one is a synchronization primitive, the other a structural expression of task ownership. The shift is from asking when the count reaches zero to asking who owns these tasks and what happens on failure.

Suspension is conditional, not magical. A coroutine avoids blocking only when the APIs it calls are genuinely suspendable; wrapping `Future.get()` in a coroutine still parks a thread, and blocking code inside `loadUser()` on the main thread still freezes the UI.

The lightweight-thread metaphor is not just imprecise; it directs attention to dispatchers and thread counts instead of the questions that actually cause bugs: scope, cancellation, failure propagation, and backpressure.

Channel capacity is a design decision, not a tuning knob. Rendezvous, bounded, and unlimited channels encode different collaboration policies, and an unlimited channel trades suspension for memory growth.

Concepts & terms
Structured concurrency
A Kotlin coroutine design principle where tasks are bound to a scope; the scope completes only after its children finish, and cancellation or failure propagates from parent to children.
Coroutine suspension
A suspendable task can pause at a suspension point without occupying a thread; when the awaited condition is ready, the coroutine resumes on an available thread.
Backpressure
The mechanism by which a bounded queue or channel slows a producer when the consumer cannot keep up, preventing unbounded memory growth.
Rendezvous channel
A Channel with no buffer; a send does not complete until a receiver is ready, so each transmission is a direct handoff.
Poison pill
A sentinel object placed in a BlockingQueue to signal the end of production; a consumer exits when it receives the sentinel.
ANR
Application Not Responding; Android's fatal condition when the main thread cannot process input or draw within a reasonable time, often caused by blocking waits.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗