很多人认为协程比线程高效,是因为:
- 更轻量;
- 更少的线程;
- 用户态切换;
- 一个线程运行几十万个协程。
这些怎么说呢,一言难尽。
其实这些并不是 Kotlin 协程改变 Android 开发方式的核心原因。
因为:
线程池同样可以复用线程;
Netty 同样可以处理海量连接;
Node.js 很早就实现了单线程高并发。
协程真正高效的地方其实是:
- 它让异步重新变成了线性代码;
- 它让线程阻塞变成协程挂起;
- 它让线程之间的同步协作变成协程之间的协作;
- 它减少了大量为了组织异步流程而引入的 JUC 同步器。
而这些,才是协程真正改变异步开发方式的地方。
一、线程时代最大的成本,其实是异步复杂度
很多人认为线程模型最大的成本是:
阻塞。
但对于绝大多数 Android 开发者来说,真正痛苦的其实是:
异步会打碎程序的控制流。
例如经典的回调写法:
kotlin
loginAsync { user ->
queryProfileAsync(user.id) { profile ->
queryVipAsync(profile.id) { vip ->
updateUi(vip)
}
}
}
业务流程其实非常简单:
text
登录
↓
查询用户信息
↓
查询会员信息
↓
更新 UI
但代码结构却变成:
text
callback
└── callback
└── callback
└── callback
于是各种问题开始出现:
- 异常处理困难;
- 生命周期难管理;
- 状态同步复杂;
- 取消逻辑难维护;
- 阅读成本急剧上升。
真正消耗开发效率的,从来不是线程本身,
而是:
异步代码失去了线性表达能力。
二、线程时代想写同步代码,往往需要各种 JUC
面对 Callback Hell,我们自然会产生一个想法:
能不能把异步代码重新写成同步代码?
线程时代当然可以。
但通常需要借助各种同步工具。
例如:
java
CountDownLatch latch = new CountDownLatch(1);
loginAsync(user -> {
result = user;
latch.countDown();
});
latch.await();
User user = result;
或者:
java
User user = future.get();
或者:
java
User user = blockingQueue.take();
本质都是:
text
异步回调
│
▼
JUC 同步器(CountDownLatch、Future、BlockingQueue......)
│
▼
线程阻塞等待
│
▼
回调通知线程继续执行
代码确实重新变成了:
java
User user = login();
Profile profile = queryProfile(user.id);
Vip vip = queryVip(profile.id);
updateUi(vip);
但是代价也非常明显。
为了获得同步代码的阅读体验,我们不得不:
- 阻塞当前线程;
- 使用额外线程承载这些等待任务,防止 ANR;
- 引入大量 JUC 同步工具来协调异步流程。
于是项目里开始出现越来越多的:
- CountDownLatch
- Future
- CompletableFuture
- BlockingQueue
- wait / notify
- synchronized
- ReentrantLock
很多时候:
这些工具并不是业务真正需要的。
它们存在的目的,仅仅只是:
把异步回调重新缝合成线性代码。
更重要的是,这些同步工具本身也存在额外成本。
例如:
- CountDownLatch 底层依赖 AQS;
- Future.get() 会阻塞线程;
- BlockingQueue 内部大量依赖 Lock、Condition 或 CAS;
- wait / notify 依赖对象监视器;
- synchronized、ReentrantLock 会带来锁竞争。
也就是说:
线程时代为了获得同步代码,不仅付出了线程阻塞的代价,还付出了大量同步器协作的代价。
三、协程最大的价值:异步同步化并且是非阻塞的
而协程第一次优雅地解决了这个问题。
kotlin
val user = login()
val profile = queryProfile(user.id)
val vip = queryVip(profile.id)
updateUi(vip)
看起来是同步代码:
text
A → B → C → D
实际上运行过程却是:
text
login()
↓
挂起协程
↓
释放线程
↓
等待网络返回
↓
恢复协程
↓
继续执行下一行代码
整个过程中:
没有:
- CountDownLatch
- Future.get()
- CompletableFuture.join()
- BlockingQueue
- wait / notify
- synchronized
- ReentrantLock
甚至没有:
kotlin
Thread.sleep()
因为协程并不是依赖这些同步工具等待结果。
suspend 的本质,是编译器生成的 Continuation(续体)+ 状态机。
当异步任务没有完成时:
text
保存执行现场
↓
挂起协程
↓
释放线程
↓
异步任务完成
↓
resume()
↓
恢复状态机继续执行
等待的是:
text
协程
而不是:
text
线程
因此:
协程并不是把 Callback 变成了 CountDownLatch,而是直接把 Callback 变成了 Continuation。
整个异步流程重新回到了线性表达。
协程完成了一件线程时代很难优雅做到的事情:
让异步代码拥有同步代码的阅读体验。
同时又保持:
非阻塞执行。
这可能才是 Kotlin 协程相比传统线程模型最大的价值。
因为它第一次真正实现了:
线性表达 + 非阻塞执行。
四、协程不仅减少锁竞争,更减少了同步器的使用
线程模型下:
多个线程同时访问共享资源时:
- synchronized
- ReentrantLock
- Condition
- wait / notify
- BlockingQueue
几乎不可避免。
不仅如此。
为了把异步重新组织成同步流程,还需要:
- CountDownLatch
- Future
- CompletableFuture
- Semaphore
也就是说:
线程时代大量代码,其实都是围绕:
线程之间如何协作。
而不是:
业务本身。
协程改变了这一点。
例如:
kotlin
val mutex = Mutex()
mutex.withLock {
updateData()
}
如果锁已经被占用:
text
挂起协程
↓
释放线程
↓
等待恢复
等待的是:
text
协程
而不是:
text
线程
因此:
- 不容易产生线程池饥饿;
- 不容易出现大量 WAITING 线程;
- 锁竞争造成的资源浪费更少;
- 调度压力明显减小。
不过,协程真正厉害的地方,并不是 Mutex 比 synchronized 更快。
而是:
很多原本需要加锁的问题,在协程模型下根本就不需要加锁。
例如:
text
多个协程
│
▼
Channel
│
▼
单个消费者
│
▼
修改共享状态
或者:
text
StateFlow
SharedFlow
Actor
共享状态逐渐变成:
单一拥有者(Single Owner)
多个任务之间:
通过消息传递进行协作。
而不是:
多个线程竞争同一把锁。
因此 Kotlin 世界越来越倾向于:
- Mutex
- Channel
- Flow
- Actor
- Semaphore
而不是:
- synchronized
- ReentrantLock
- BlockingQueue
- wait / notify
因为:
协程时代强调的是协作,而不是竞争。
真正减少的,不只是锁竞争。
更重要的是:
很多原本必须依赖锁和同步器解决的问题,在协程模型下根本就不需要出现。
五、结构化并发进一步降低了复杂度
线程时代:
任务生命周期和业务生命周期往往是分离的。
例如:
text
页面销毁了
线程还在执行。
于是经常出现:
- 页面退出后回调回来更新 UI;
- Future 无法取消;
- 后台任务泄漏;
- 生命周期错乱。
而协程:
text
ViewModelScope
├── task1
├── task2
└── task3
父协程取消:
text
所有子协程自动取消。
生命周期天然绑定。
大量同步和状态管理问题因此消失。
这也是 Kotlin 协程另一个容易被忽视的优势:
结构化并发降低了系统复杂度。
六、协程并没有消灭回调
很多人第一次接触协程时都会有一种感觉:
suspend 函数没有回调。
其实并不是。
如果反编译一个 suspend 函数:
kotlin
suspend fun login(): User
最终大致会变成:
java
Object login(Continuation<? super User> continuation)
本质仍然是:
text
请求完成
↓
回调 Continuation
↓
恢复协程执行
区别仅仅在于:
以前:
text
开发者维护回调链。
开发者维护状态机。
现在:
text
编译器维护回调链。
编译器维护状态机。
所以从某种意义上来说:
协程并没有消灭回调。
它只是:
把回调隐藏到了编译器和运行时之中。
七、协程真正优化的不是线程,而是异步编程
很多文章喜欢说:
协程比线程快。
实际上这并不准确。
真正执行代码的始终都是线程。
协程优化的从来不是:
text
CPU 计算速度
而是:
text
异步程序的组织成本
它减少的是:
- Callback Hell;
- 状态机维护成本;
- JUC 同步器的使用;
- 锁竞争成本;
- 阻塞等待成本;
- 生命周期管理成本。
因此更准确的说法应该是:
协程不是让程序运行得更快。
而是:
让异步程序变得更简单、更稳定、更容易维护。
八、总结
线程池解决的是:
text
线程复用问题。
协程解决的是:
text
异步表达问题。
因此:
协程比线程高效,并不是因为它创建得更轻,也不是因为它使用了更少的线程。
真正发生变化的是:
它重新设计了异步程序的组织方式。
它让:
- Callback 变成了 Continuation;
- 手写状态机变成了编译器生成状态机;
- 线程阻塞变成了协程挂起;
- JUC 同步器变成了协程协作;
- 线程竞争变成了消息传递与单一所有者模型。
所以,协程真正优化的,从来不是 CPU,也不是线程。
而是:
异步程序的组织成本。
它让开发者不再把大量精力浪费在:
- 如何等待;
- 如何同步;
- 如何加锁;
- 如何管理回调;
- 如何维护状态;
而是把注意力重新放回:
业务本身。
所以,我认为Kotlin 协程真正伟大的地方,并不是它比线程快了多少。
而是它:
用同步代码的思维,编写异步程序。
用非阻塞的挂起机制,获得同步代码般的可读性。
协程并不是消灭所有异步开销,而是减少为了"让异步流程表现得像同步代码"而引入的大量同步控制成本。
协程减少的不是 Thread 的数量,而是:
CountDownLatch等等待同步器;Future/CompletableFuture等异步组合机制;- 回调嵌套带来的复杂控制流;
- 围绕线程同步产生的锁、状态管理和异常处理代码。
所以本质上协程比线程高效的原因是减少了异步复杂度。
参考
从 Callback 到 Coroutines:Android 异步并发方案的演进
"结构化"这个词,本质上就是------把混乱的东西变成有组织、有规则、有边界的东西
从"调用方的如履薄冰"到"接口的天然语义":Room/DataStore/Retrofit 的启示
现代 Android 官方为什么更推荐 Repository 暴露 suspend fun,而不是在内部 launch
# Android 架构指南之Data 层不要再暴露 start/stop 了:用 Flow 接管生命周期
别再 launch(IO) 了:协程线程切换的 3隐藏反模式
从送外卖看Android Clean架构:为什么老板不需要知道外卖员开什么车