Kotlin Coroutines: '协' Means Collaboration, Not Lightweight Threads
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.'
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.
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.