协程基础概念:从线程到协程,到底切换了什么?
各位好。先聊个我自己的经历。几年前我刚从 Java 转 Kotlin,看到 launch 和 async 时第一反应是:这不就是线程池封装吗?结果第一次上线就遇到了启动时序问题——子协程还没执行完,主线程就已经走了。排查了一整天,才意识到我对协程的「本质」理解太浅了。
所以第一章,咱们先把这个地基打扎实。你只有懂了协程到底是什么、怎么工作的,后面那些启动顺序、取消超时、并发逃逸的问题才看得透。
1.1 协程的定义:轻量级线程?不,是轻量级任务上下文
官方说协程是「轻量级线程」。我其实不太喜欢这个比喻。因为线程再轻量也是操作系统级的资源,而协程纯粹是用户态的。
- 协程:一段可以被挂起和恢复的代码片段。挂起时不阻塞线程,恢复时从上次离开的地方接着跑。
- 关键区别:线程的切换由内核调度器决定,协程的切换由开发者写的挂起点(
suspend函数)决定。
你想想看,如果每个网络请求都开一个线程,你的手机服务器可能早就撑爆了。协程允许你在同一个线程里跑成千上万个并发任务——只要它们都在等待 I/O。
Room 的挂起函数会自动切到后台线程,主线程只做挂起等待,根本不会卡死。
1.2 优势对比:协程 vs 线程 vs 回调
写两个例子你就明白了。
线程的方式:
// 假设连续两次网络请求
Thread {
val result1 = api.fetch1() // 阻塞当前线程
val result2 = api.fetch2(result1)
}.start()
这个线程在等待网络时啥也干不了。线程数一多,上下文切换成本就上来了。
回调的方式:
api.fetch1 { result1 ->
api.fetch2(result1) { result2 ->
// 继续处理
}
}
回调地狱来了。而且错误处理分散在各处,你很难在中间加个重试逻辑。
协程的方式:
suspend fun fetchBoth(): String {
val result1 = api.fetch1() // 挂起,不阻塞线程
val result2 = api.fetch2(result1)
return result2
}
代码看起来就像同步的,但底层用了挂起机制。线程没被占用,可以干别的事。
| 维度 | 线程 | 回调 | 协程 |
|---|---|---|---|
| 资源开销 | 高(栈 1MB+) | 低 | 极低(对象级别) |
| 可读性 | 一般 | 差(回调地狱) | 优秀(类似同步) |
| 取消与超时 | 困难(interrupt() 不可靠) | 需要手动维护状态 | 原生支持 cancel() |
| 错误处理 | try-catch 正常 | 容易遗漏 | try-catch 正常 |
说白了:协程把「并发」的复杂度从「资源调度」变成了「任务编排」。你只需要关心业务逻辑,调度由协程框架帮你搞定。
1.3 协程与线程的关系:不是替代,是协作
我见过有人面试时说「协程完全替代线程」。这个观点有问题。协程最终还是要跑在线程上的。
- 协程是任务单元,线程是执行载体。多个协程可以共享一个线程,通过挂起/恢复实现协作式调度。
- 协程挂起时,线程不空闲? 不,线程会去跑其他协程。这就是高效的原因。
- 协程可以切换线程,比如
withContext(Dispatchers.IO)把协程切到 IO 线程做阻塞操作。
1.4 协程的工作原理:挂起与状态机
协程是怎么做到「挂起不阻塞」的?核心是编译器的黑魔法——协程会变成一个状态机。
看一个最简单的例子:
suspend fun foo() {
println("1")
delay(1000) // 挂起点
println("2")
}
Kotlin 编译后,实际会生成类似这样的状态机:
// 伪代码
class FooContinuation : Continuation {
var label = 0
var result: Any? = null
fun resume() {
when (label) {
0 -> {
println("1")
label = 1
// 调用 delay,传入 continuation 自身
delay(1000, this)
// 注意这里没有 return;delay 会返回 COROUTINE_SUSPENDED
}
1 -> {
println("2")
}
}
}
}
当 delay(1000) 被调用时,如果时间未到,它会返回一个特殊标记 COROUTINE_SUSPENDED,并把当前协程的状态(label=1、局部变量)存在 continuation 对象里。然后线程就可以去执行其他协程。1秒后,定时器触发,调用 continuation 的 resume(),从 label=1 继续执行。
1.5 核心知识体系:一张图讲清楚
下面这张图总结了协程基础概念的几个关键节点和它们的关系。你以后做深度调优时,可以回来对照这张图。
1.6 小结
协程不是魔法。它就是编译器帮你生成的状态机,配合调度器在合适的线程上跑。记住这几个要点,后续章节的「启动时序」问题就会好理解很多:
- 协程的创建(
launch/async)本身是同步的,启动时机由调度器决定。 - 挂起点是协程主动交回执行权的时刻,线程才能切换去跑其他协程。
- 同一个协程内顺序执行,不同协程之间是并发(或串行)取决于启动方式。