KOTLIN COROUTINES · 原理篇;现在发现AI很方便,可以把读书笔记,一股脑丢给他,然后,让他生成一个文章。这篇文章,就是根据我的读书笔记生成的。
协程如何挂起
从 suspendCoroutine、Continuation 到回调桥接与恢复机制
挂起不是让函数消失,也不是让线程停住;它是把协程的下一步保存为 Continuation,释放当前线程,并在结果到达后从原位置继续。
重新整理版 · 含源码拆解、执行流程与工程注意事项
本章目标
本章围绕一个核心问题展开:Kotlin 协程执行到挂起函数时,究竟保存了什么、释放了什么,又如何在稍后继续运行。读完后,你应当能够解释 suspend、suspendCoroutine 和 Continuation 的关系,并能把一个回调式 API 安全地改造成挂起函数。
- 区分“挂起协程”和“阻塞线程”,避免把 suspend 理解为自动切线程。
- 理解 suspendCoroutine 的 Lambda 为什么在挂起前执行,以及 Continuation 从哪里获得。
- 理解 resume(value) 与 resumeWithException(error) 如何分别产生返回值和异常。
- 掌握使用 suspendCancellableCoroutine 包装旧式回调 API 的基本结构。
- 理解为什么挂起的是整个协程执行链,而不是某一个孤立的函数。
**一句话模型 **挂起 = 保存继续执行所需的状态 + 让出线程;恢复 = 把结果送回挂起点 + 继续执行后续代码。
1 先建立正确模型:挂起不等于阻塞
挂起函数是 Kotlin 协程最具代表性的能力。一个协程可以在函数执行到一半时暂停,并在将来的某个时刻从同一逻辑位置继续。暂停期间,执行它的线程可以转去处理其他任务。
可以把 Continuation 想成“续跑凭证”:它记录恢复之后应该从哪里继续,以及继续执行所需要的局部状态。不过这个类比只帮助理解控制流,Continuation 在实现上是一个对象,并非完整复制线程或整个进程。
| 对比项 | 阻塞线程 | 挂起协程 |
|---|---|---|
| 等待期间 | 线程停在那里,不能执行别的任务 | 协程保存状态并让出线程 |
| 资源占用 | 持续占用线程栈和调度资源 | 仅保留 Continuation 等少量状态 |
| 恢复位置 | 仍在被阻塞的线程上继续 | 可由调度器安排到同一或其他线程 |
| 典型 API | Thread.sleep、阻塞式 I/O | delay、挂起式网络/数据库 API |
**准确表述 **“协程挂起时不消耗资源”并不严谨。它不再占用线程和 CPU 执行时间,但 Continuation、局部变量及上下文仍会占用少量内存。
另一个常见误解是:suspend 会自动把代码切换到后台线程。事实并非如此。suspend 只表示函数具备挂起当前协程的能力;是否真的挂起、在哪个线程恢复,由具体实现和 CoroutineDispatcher 决定。
2 最小实验:只挂起,不恢复
先从最小程序观察挂起点。suspend main 让程序入口本身处于协程中,因此可以直接调用其他挂起函数。
示例 1 协程被挂起后没有恢复
import kotlin.coroutines.suspendCoroutine
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { }
println("After")
}
运行结果只有 Before。原因不是 println 失效,而是当前协程在 suspendCoroutine 处暂停后,再也没有人恢复它。因此 suspendCoroutine 后面的语句没有机会执行,main 也无法正常完成。
- main 协程打印 Before。
- 调用 suspendCoroutine<Unit>。
- 传入的 Lambda 在真正挂起前执行,但 Lambda 是空的,没有保存或恢复 Continuation。
- 当前协程停在挂起点;After 位于恢复路径上,因此不会打印。
**关键点 **挂起点之后的代码并没有丢失,而是暂时不可达;只有 Continuation 被恢复,程序才会继续。
3 suspendCoroutine 如何交出 Continuation
suspendCoroutine 接收一个 Lambda。这个 Lambda 会在协程挂起之前被调用,并获得当前挂起点对应的 Continuation。概念化的函数类型可以写成:
概念签名
suspend fun <T> suspendCoroutine(
block: (Continuation<T>) -> Unit
): T
这里的 T 同时出现在三处:suspendCoroutine 的返回类型、Continuation<T> 的泛型参数,以及 resume 时必须提供的值类型。
| 挂起表达式 | 恢复调用 | 表达式结果 |
|---|---|---|
| suspendCoroutine<Unit> | resume(Unit) | Unit |
| suspendCoroutine<Int> | resume(42) | 42 |
| suspendCoroutine<String> | resume("text") | "text" |
| suspendCoroutine<User> | resume(user) | User 对象 |
Lambda 必须在挂起前获得 Continuation,因为一旦 suspendCoroutine 已经挂起,当前协程就不能继续执行后面的代码去寻找恢复方式。通常,Lambda 会把 Continuation 交给定时器、网络回调、系统监听器或其他异步组件。
4 立即恢复:看清 resume 的含义
示例 2 在 suspendCoroutine 内立即恢复
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
continuation.resume(Unit)
}
println("After")
}
输出为 Before、After。resume(Unit) 把成功结果送回挂起点,因此 suspendCoroutine<Unit> 完成,控制流继续进入下一条语句。由于恢复发生得非常早,这个例子可能不会表现出可观察的长时间挂起,但它清楚展示了恢复通道。
resume 与 resumeWith 的关系
continuation.resume(value)
// 可以理解为调用统一入口:
continuation.resumeWith(Result.success(value))
现代 Kotlin 的 Continuation 核心入口是 resumeWith(Result<T>)。标准库提供的 resume(value) 和 resumeWithException(error) 是更易读的扩展函数,分别构造成功与失败的 Result。
5 延迟恢复:线程如何被释放和重新使用
为了观察真正的等待,可以让定时调度器在一秒后恢复 Continuation。重点不是 ScheduledExecutorService 本身,而是“登记未来动作后立即挂起”的模式。
示例 3 使用调度器延迟恢复
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
private val scheduler =
Executors.newSingleThreadScheduledExecutor { task ->
Thread(task, "scheduler").apply {
isDaemon = true
}
}
suspend fun waitOneSecond() {
suspendCoroutine<Unit> { continuation ->
scheduler.schedule(
{ continuation.resume(Unit) },
1,
TimeUnit.SECONDS
)
}
}
suspend fun main() {
println("Before: ${Thread.currentThread().name}")
waitOneSecond()
println("After: ${Thread.currentThread().name}")
}
5.1 逐段解释
newSingleThreadScheduledExecutor 创建一个可复用的调度线程。它像“闹钟管理器”一样保存定时任务,而不是每次等待都新建一个线程。isDaemon = true 表示当 JVM 只剩守护线程时,调度线程不会单独阻止程序退出。
waitOneSecond 调用 suspendCoroutine 后,把 Continuation 捕获到 schedule 的 Lambda 中。登记完成后,当前协程挂起;一秒到期,scheduler 线程执行 resume(Unit),协程从 waitOneSecond 返回处继续。
输出:
Before: main
After: scheduler
5.1.1 为什么恢复之后的代码,执行在scheduler?是因为main所在的线程已经结束了吗?
不是因为 main 线程已经结束了,而是因为:
Continuation 在哪个线程上被调用 resume(),没有调度器介入时,后续代码通常就在哪个线程继续执行。
具体流程如下。
scheduler.schedule(
{ continuation.resume(Unit) },
1,
TimeUnit.SECONDS
)
ScheduledExecutorService 使用名为 "scheduler" 的线程执行这个 Lambda:
{
continuation.resume(Unit)
}
因此,一秒后实际调用 resume() 的线程是:
scheduler
当前协程又没有配置 CoroutineDispatcher,所以 resume(Unit) 会直接驱动协程的状态机继续执行。执行顺序可以理解成:
main 线程
│
├── println("Before")
├── 调用 waitOneSecond()
├── suspendCoroutine 挂起协程
└── 等待整个 suspend main 完成
一秒后:
scheduler 线程
│
├── continuation.resume(Unit)
├── waitOneSecond() 返回
├── 从 waitOneSecond() 后面继续
└── println("After")
所以:
println("After: ${Thread.currentThread().name}")
输出:
After: scheduler
resume() 恢复的是协程,不是原线程
resume() 的含义不是:
找到原来的 main 线程,并叫醒它。
而是:
让协程从保存的挂起点继续执行。
协程的逻辑执行状态保存在 Continuation 中,并不绑定某个固定线程。因此它可以:
- 在 main 线程开始;
- 在某个位置挂起;
- 在 scheduler 线程恢复;
- 以后又被调度到其他线程。
线程发生了变化,但仍然是同一个协程的逻辑执行过程。
main 线程发生了什么?
对于 suspend fun main(),Kotlin 会生成一个普通 JVM 入口,用于启动这个挂起函数并等待它完成。
当协程挂起时,入口线程通常还没有真正结束,而是在等待整个 suspend main 完成。等 scheduler 线程恢复协程并执行完 "After" 后,根 Continuation 才会收到完成通知,程序随后退出。
所以真正的原因不是:
main 线程已经结束,所以只能使用 scheduler
而是:
scheduler 调用了 resume
+
协程上下文中没有 Dispatcher 把恢复操作切回 main
使用 runBlocking 会怎样?
可以使用 runBlocking 提供绑定到当前线程的事件循环:
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
println("Before: ${Thread.currentThread().name}")
waitOneSecond()
println("After: ${Thread.currentThread().name}")
}
通常输出:
Before: main
After: main
此时,虽然 continuation.resume(Unit) 仍由 scheduler 线程调用,但 runBlocking 的协程上下文包含调度机制,恢复操作会被投递回 main 线程的事件循环。
大致过程是:
scheduler 调用 resume
↓
发现 Continuation 带有调度器
↓
不直接执行后续代码
↓
把恢复任务投递给 main 的事件循环
↓
main 线程继续执行
Android 中的情况
如果协程运行在:
viewModelScope.launch(Dispatchers.Main) {
println(Thread.currentThread().name)
waitOneSecond()
println(Thread.currentThread().name)
}
通常前后都会在 Android 主线程:
main
main
因为 Dispatchers.Main 会拦截恢复操作。即使网络线程或定时器线程调用 resume(),后续代码也会被重新调度回主线程。
最终结论:
没有 CoroutineDispatcher 时,调用 resume() 的线程通常直接执行挂起点之后的代码;存在调度器时,恢复会按照协程上下文重新调度。resume() 恢复的是协程,不是某个固定线程。
**线程提示 **在这个低层示例中,恢复后的代码可能直接运行在 scheduler 线程。真实的 kotlinx.coroutines 会结合 ContinuationInterceptor / CoroutineDispatcher,把恢复安排到合适的线程。
5.2 为什么实际代码直接使用 delay
推荐写法
import kotlinx.coroutines.delay
suspend fun main() {
println("Before")
delay(1_000)
println("After")
}
delay 已经提供非阻塞等待,并与协程取消、调度器和测试时间控制集成。手写 ScheduledExecutorService 的价值主要在于理解原理,而不是替代协程库的成熟实现。
6 使用一个值恢复协程
协程经常因为缺少某个结果而挂起。当结果到达时,resume 不只是发出“继续”信号,还可以把数据作为挂起表达式的返回值送回来。
示例 4 恢复时提供返回值
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun main() {
val number: Int = suspendCoroutine { cont ->
cont.resume(42)
}
val text: String = suspendCoroutine { cont ->
cont.resume("Some text")
}
println(number) // 42
println(text) // Some text
}
以 number 为例:编译器从左侧类型和 resume(42) 推断出 T 为 Int。resume(42) 之后,整个 suspendCoroutine 表达式的结果就是 42,所以它可以像普通函数返回值一样赋给 number。
**类型闭环 **suspendCoroutine<T>、Continuation<T>、resume(T) 与最终表达式结果 T 必须保持一致。
7 把回调 API 改造成挂起函数
回调式 API 往往把结果嵌套在 Lambda 中。通过 suspendCancellableCoroutine,可以把“结果到达后执行回调”改写为“在这里挂起,结果到达后从这里返回”,从而得到更线性的业务代码。
示例 5 最基本的回调桥接
data class User(val name: String)
// 旧式回调 API
fun requestUser(callback: (User) -> Unit) {
// 异步请求完成后:callback(user)
}
// 包装后的挂起 API
suspend fun awaitUser(): User =
suspendCoroutine { continuation ->
requestUser { user ->
continuation.resume(user)
}
}
suspend fun main() {
println("Before")
val user = awaitUser()
println(user)
println("After")
}
7.1 执行链拆解
- main 调用 awaitUser,业务代码看起来仍然是顺序结构。
- awaitUser 发起异步请求,并把 Continuation 间接交给回调。
- 等待网络期间,awaitUser 所在协程挂起,底层线程可以执行其他任务。
- 请求成功后,回调获得 User,并调用 continuation.resume(user)。
- suspendCoroutine 表达式产生 User,awaitUser 返回,main 从 val user = awaitUser() 之后继续。
**命名建议 **如果回调函数也叫 requestUser,建议把挂起包装命名为 awaitUser、getUser 或 requestUserSuspend,以减少重载阅读成本。
8 生产级桥接:成功、失败与取消
真实 API 不只会成功,也可能失败或被取消。工程代码通常应优先使用 suspendCancellableCoroutine,让协程取消能向下传递到底层请求。
示例 6 可取消的回调桥接
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
suspend fun awaitUser(): User =
suspendCancellableCoroutine { continuation ->
val call = api.requestUser(
onSuccess = { user ->
if (continuation.isActive) {
continuation.resume(user)
}
},
onError = { error ->
if (continuation.isActive) {
continuation.resumeWithException(error)
}
}
)
continuation.invokeOnCancellation {
call.cancel()
}
}
8.1 每一部分负责什么
- onSuccess:用 User 正常恢复,挂起函数返回 User。
- onError:用异常恢复,调用 awaitUser 的位置表现得像直接抛出了异常。
- isActive:避免协程已经取消后仍尝试恢复;但它不是所有竞态条件的万能锁。
- invokeOnCancellation:在上层取消协程时,同步取消底层网络请求,避免无意义工作。
最重要的不变量是:同一个 Continuation 只能完成一次。底层 API 若可能重复回调,包装层必须保证只接受第一个终态,否则重复 resume 可能抛出 IllegalStateException。对于复杂竞态,应遵循所用库的官方桥接方式,而不是仅依赖一次 isActive 检查。
**工程优先级 **Retrofit、Room 等库已经原生支持挂起函数时,应直接使用其 suspend API。只有面对遗留回调、监听器或第三方 SDK 时,才需要手动桥接。
9 使用异常恢复
普通函数可能返回值,也可能抛出异常;挂起表达式保持同样的调用体验。resumeWithException(error) 会让异常从原挂起点重新出现,因此调用者可以用常规 try/catch 处理。
示例 7 异常从挂起点重新抛出
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class MyException : RuntimeException("Just an exception")
suspend fun main() {
try {
suspendCoroutine<Unit> { cont ->
cont.resumeWithException(MyException())
}
} catch (e: MyException) {
println("Caught!")
}
}
这并不是另一个线程直接跳进当前调用栈抛异常。更准确地说,恢复时传入 Result.failure(error),协程状态机在恢复挂起点时检查结果,并在该逻辑位置重新抛出异常。因此 try/catch 可以像处理普通函数异常一样工作。
失败恢复的统一入口
continuation.resumeWithException(error)
// 概念上等价于:
continuation.resumeWith(Result.failure(error))
10 挂起的是协程,不是函数
挂起函数只是一个能够挂起当前协程的函数。真正暂停的是包含完整调用链的协程。如果把 Continuation 保存起来,却把 resume 写在同一个挂起点之后,那么恢复代码永远没有机会执行。
示例 8 同一协程无法执行挂起点之后的自救代码
// 错误示例:不要这样保存全局 Continuation
var saved: Continuation<Unit>? = null
suspend fun suspendAndSave() {
suspendCoroutine<Unit> { cont ->
saved = cont
}
}
suspend fun main() {
println("Before")
suspendAndSave()
saved?.resume(Unit) // 永远执行不到
println("After")
}
程序只打印 Before。它形成了一个循环依赖:协程必须先恢复,才能执行 saved?.resume(Unit);但这行代码正是恢复协程所需要的动作。
**风险 **把 Continuation 放在全局变量中还可能造成内存泄漏、重复恢复和并发竞态。示例只用于说明控制流,不应作为业务实现。
恢复必须来自挂起协程之外,例如网络回调、定时器、系统事件,或者另一个仍能运行的协程。下面的代码能够运行,但仍只是教学演示:
示例 9 由另一个协程触发恢复(仅作原理演示)
var saved: Continuation<Unit>? = null
suspend fun suspendAndSave() {
suspendCoroutine<Unit> { cont -> saved = cont }
}
suspend fun main() = coroutineScope {
println("Before")
launch {
delay(1_000)
saved?.resume(Unit)
saved = null
}
suspendAndSave()
println("After")
}
11 从底层看:状态机与 Continuation
编译器会把包含挂起点的函数转换为近似状态机。函数开始位置是一个状态,每个可能挂起的调用之后又是一个状态。Continuation 对象保存当前状态编号,以及恢复后仍然需要的局部变量。
源代码视角
suspend fun loadAndShow() {
val user = awaitUser() // 挂起点 1
val news = awaitNews(user) // 挂起点 2
show(news)
}
状态机视角(概念化)
// 极度简化的概念伪代码,并非真实反编译结果
fun loadAndShow(cont: LoadAndShowContinuation): Any? {
when (cont.label) {
0 -> {
cont.label = 1
val result = awaitUser(cont)
if (result === COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
cont.user = result as User
}
1 -> {
cont.user = cont.result.getOrThrow() as User
}
}
cont.label = 2
val news = awaitNews(cont.user, cont)
if (news === COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
show(news as List<News>)
return Unit
}
11.1 为什么返回类型看起来像 Any?
底层函数既可能立即得到真实结果,也可能返回内部标记 COROUTINE_SUSPENDED。因此概念上的 JVM 形式需要同时容纳业务结果和挂起标记,常被解释为返回 Any?。日常 Kotlin 源码仍然保持精确的声明类型。
11.2 Continuation 如何形成调用链
被调用挂起函数获得调用者的 Continuation,并在完成时把结果向上恢复。多个 Continuation 因而形成一种可恢复的调用链,承担了传统线程调用栈在挂起期间无法保留的那部分职责。恢复时,结果沿这条链返回;某一层完成后,再继续恢复上一层。
**不要过度类比 **Continuation 不是线程栈的完整序列化副本。它保存的是编译器判定在挂起后仍然需要的状态。协程可以换线程恢复,正是因为逻辑执行状态不再绑死在线程栈上。
12 常见误区与排查清单
- 误区:suspend 函数一定会挂起。事实:它只是允许挂起,也可能立即返回。
- 误区:suspend 自动切到后台线程。事实:线程选择由调度器和具体 API 决定。
- 误区:挂起后完全不占内存。事实:Continuation 和必要状态仍需保留。
- 误区:只要调用 resume 就安全。事实:同一个 Continuation 只能完成一次。
- 误区:取消协程会自动取消所有第三方请求。事实:桥接时通常要显式调用底层 cancel。
- 误区:把 Continuation 存成全局变量即可稍后恢复。事实:这容易泄漏、竞态并破坏结构化并发。
包装回调 API 前的检查
- 回调是否保证只调用一次?如果不是,如何原子地选择唯一终态?
- 成功、失败和取消三条路径是否都能完成 Continuation?
- 协程取消后,能否取消底层网络、数据库或监听任务?
- 回调可能在哪个线程触发?恢复后是否需要特定 Dispatcher?
- 底层库是否已经提供官方 suspend 版本?如果有,应优先使用。
13 完整心智模型
把整个过程连起来,可以得到下面这条主线:
- 协程在某个挂起函数中需要等待外部结果。
- 挂起函数通过 Continuation 表示“结果回来后从哪里继续”。
- 它登记回调、定时器或其他异步动作,然后返回挂起标记。
- 当前线程不再为这次等待停留,可以执行其他任务。
- 外部结果到达后,异步组件调用 resume(value) 或 resumeWithException(error)。
- 协程状态机读取保存的状态和局部数据,从挂起点后继续。
- 如果有 CoroutineDispatcher,恢复会按照上下文重新调度到合适线程。
**最终结论 **挂起机制把“等待”从占住线程的行为,变成保存少量状态、稍后继续的控制流。这正是协程能够以顺序代码表达异步任务,同时保持高并发效率的基础。
附录 核心 API 速查
核心调用关系
// 不支持取消的底层桥接原语
suspendCoroutine<T> { continuation ->
// 完成时:continuation.resume(value)
}
// 支持协程取消的常用桥接原语
suspendCancellableCoroutine<T> { continuation ->
continuation.invokeOnCancellation {
// 取消底层任务
}
}
// 成功恢复
continuation.resume(value)
// 失败恢复
continuation.resumeWithException(error)
// 统一底层入口
continuation.resumeWith(Result.success(value))
continuation.resumeWith(Result.failure(error))