Kotlin 协程原理详解(挂起 / 恢复 / 取消 / 调度 / 异常处理 全面梳理)
1. 整体架构 — 协程是什么(用一句话)
Kotlin 协程是一套轻量级的用户态协作式任务框架,借助编译器把 suspend 函数变成带 Continuation 的状态机,使得异步逻辑可以像同步代码一样写,但底层不阻塞线程:挂起点会保存执行状态并返回,恢复时通过 Continuation.resumeWith(...) 继续执行。
2. 核心概念(名词速查)
- suspend 函数:可以在其中“挂起”执行的函数(编译器转成状态机并隐式接受
Continuation<T>)。 - Continuation<T>:协程的“恢复句柄”,接口包含
resumeWith(Result<T>)。保存了要恢复时的上下文和下一步执行信息。 - CoroutineContext:协程运行时的集合(
Job、CoroutineDispatcher、CoroutineName、CoroutineExceptionHandler等元素)。 - Job / SupervisorJob:协程的生命周期句柄,支持
cancel()、join()、父子关系与结构化并发策略。 - CoroutineDispatcher:如何把挂起/恢复的执行调度到线程(或线程池)上(例如
Dispatchers.Default、IO、Unconfined、自定义 dispatcher)。 - suspendCoroutine / suspendCancellableCoroutine:原语,用于从回调/回调式 API 构造挂起点(可取消版本会在 Job 取消时触发回调)。
- Structured Concurrency:父协程作用域管理子协程,保证不会遗留孤儿协程,异常/取消沿 Job 层级传播。
- CancellationException:取消以异常形式传播但通常被当作正常控制流处理(不会视为未捕获异常)。
3. 编译器视角:suspend 如何实现
suspend fun foo(): T 在 JVM 上被编译器改写为: foo(Continuation<T> continuation) —— 即把恢复点(状态)封装到 Continuation 中。编译器将 suspend 点拆成状态机(类似 Kotlin 的协程状态机或 JavaScript transpile 的异步状态机):
- 每遇到
suspend点,函数会把当前局部变量和下一步状态保存在Continuation(或其生成的子类)里,然后返回(通常是COROUTINE_SUSPENDED标记)。 - 当某处恢复该
Continuation(resumeWith),状态机会根据保存的状态继续运行到下一个挂起点或结束。
直观理解:
suspend不是“阻塞线程”,而是把执行切片保存起来并把控制权交回运行时,运行时在合适时机把切片(Continuation)拿出来恢复。
4. 挂起(suspension)的两种常见实现方式
4.1 使用已有 suspend API
像 delay()、withContext() 等是已经实现好的 suspend 函数;它们自身内部会决定是否立即返回 COROUTINE_SUSPENDED 并把 Continuation 保存到某个调度队列或计时器中。
4.2 从回调构建挂起点:suspendCoroutine 与 suspendCancellableCoroutine
suspend fun awaitCallback(): String = suspendCoroutine { cont ->
someAsyncApi { result, error ->
if (error != null) cont.resumeWithException(error)
else cont.resume(result)
}
}suspendCoroutine:不会自动与协程取消关联。若协程被取消,回调仍可能触发(要手动处理取消)。suspendCancellableCoroutine:- 提供
cont.invokeOnCancellation { ... }注册取消回调,能在 Job 被取消时执行资源释放或取消底层调用(比如取消网络请求)。 - 是构建“可取消挂起”的推荐方案。
- 提供
5. 恢复(resume)机制
Continuation.resumeWith(result) 被调用时:
ContinuationInterceptor(通常是Dispatcher)可能拦截该Continuation,把恢复动作封装并提交到线程池或 Runnable 队列;- 恢复动作执行:恢复点的代码继续执行直到下一个
suspend或结束; - 如果
resumeWith的Result是失败(异常),会抛出异常到挂起点处,触发try/catch或由协程上下文处理。
重要:resumeWith 是线程安全的,且只应调用一次(重复调用会被忽略或抛出)。
6. 调度(Dispatchers)与 ContinuationInterceptor
CoroutineDispatcher是ContinuationInterceptor的实现:负责将恢复动作调度到哪儿去运行。常见实现:
Dispatchers.Default—— 基于共享的工作窃取线程池(类似ForkJoinPool)。Dispatchers.IO—— 可扩展线程池用于阻塞 IO(大小高于 Default)。Dispatchers.Main—— Android / UI 线程。Dispatchers.Unconfined—— 不受特定线程约束:首次在调用线程运行,遇到挂起后恢复在发起恢复的线程(主要用于调试 / 某些特殊场景)。
自定义 Dispatcher:实现
CoroutineDispatcher并覆盖dispatch(context, block)即可。也可实现Delay接口提供定时调度支持。
7. 取消(Cancellation)原理与实践
7.1 基本原理:协作式取消
- Kotlin 协程的取消是 *协作式- 的:
Job.cancel()会把Job标记为取消,但不会强行中断线程或强制停止执行体。运行中的协程需要在“可取消点”检测状态并主动结束。 - 可取消点包括:
suspend函数(如yield()、delay()、withContext())、select、以及你在自定义挂起点时手动检查isActive或coroutineContext[Job]?.isCancelled。
7.2 CancellationException
- 取消会以
CancellationException的形式传播(内部实现通常调用resumeWithException(CancellationException()))。 CancellationException被视为正常控制流:不会被CoroutineExceptionHandler视为未捕获致命异常(默认情况下)。在try { ... } catch (e: Throwable)时需注意捕获并区分。
7.3 suspendCancellableCoroutine 的优势
suspendCancellableCoroutine在cont.invokeOnCancellation { }中允许你在协程被取消时执行底层清理,比如取消一个 HTTP 请求。- 如果你使用
suspendCoroutine,并且底层回调不支持取消,你需要在回调中手动检查isActive或cont.context[Job]?.isCancelled。
7.4 强制清理:NonCancellable
在 finally 中,如果你需要在取消时仍然执行某些挂起操作(比如关闭资源),可以用 withContext(NonCancellable) { ... }。
try {
// work
} finally {
withContext(NonCancellable) {
// 安全执行挂起清理
}
}8. 结构化并发(Structured Concurrency)与 Job 层级
CoroutineScope.launch {}、coroutineScope {}等是结构化并发 API,父协程会等待其子协程完成(coroutineScope会阻塞直到所有子协程结束)。- 父取消会传播给子:
parent.cancel()会取消子Job(协作式)。 SupervisorJob与supervisorScope:允许子协程失败不影响同级其它子协程(即父不会因为单个子失败而取消其它子)。
示例对比:
coroutineScope {
launch { fail() } // 如果抛异常,会取消整个 coroutineScope 的其它子协程
launch { doWork() }
}
supervisorScope {
launch { fail() } // 失败不会影响其它同级子协程
launch { doWork() }
}9. 异常传播与 CoroutineExceptionHandler
异常传播规则:
- 对于 根协程(root coroutine,直接由
GlobalScope.launch等启动),未捕获的异常会由CoroutineExceptionHandler处理。 - 对于 结构化并发(如
launch在coroutineScope内),子协程未捕获异常会向上抛,最终导致父协程取消并传播到整个作用域,直至根或被捕获。
- 对于 根协程(root coroutine,直接由
CoroutineExceptionHandler只处理最终未被捕获且到达顶层的异常,不能被async引发的异常(async抛出的异常在调用await()时才会抛出)。常见规则:launch的异常是“立即”传播的(会取消父),最终交由CoroutineExceptionHandler处理;async的异常会被封装在Deferred,只有await()时抛出(也可视为“延迟异常”)。
10. 关键 API 的内部行为(withContext / async / launch / runBlocking)
withContext(dispatcher) { ... }:是个 suspend 函数,会切换CoroutineContext(包括 Dispatcher),执行 block 并在 block 内可能挂起多次,直到 block 完成后返回结果。withContext会把当前协程挂起并在新的 dispatcher 上恢复执行。launch { ... }:立即返回Job。内部创建Continuation并提交初始执行任务给Dispatcher。async { ... }:返回Deferred<T>(继承自 Job)。异常在await()时抛出(或在超出作用域时通过异常传播取消父)。runBlocking:阻塞当前线程直到协程体完成,主要用于主函数和测试。
11. 可取消的挂起实现模式(示例)
11.1 基本 suspendCoroutine(不可自动响应取消)
suspend fun awaitCallback(): String = suspendCoroutine { cont ->
val callback = object : Callback {
override fun onResult(result: String) {
cont.resume(result)
}
override fun onError(t: Throwable) {
cont.resumeWithException(t)
}
}
register(callback)
// 如果协程被取消,这里没有自动清理注册;需要额外处理
}11.2 推荐:suspendCancellableCoroutine(带取消钩子)
suspend fun awaitCancellable(): String = suspendCancellableCoroutine { cont ->
val call = createCancelableCall()
call.enqueue { result -> cont.resume(result) }
cont.invokeOnCancellation { cause ->
call.cancel() // 在 Job 取消时取消底层请求
}
}12. 协程状态机 & 内部 Result 流程(更底层)
Continuation.resumeWith(result)接收Result<T>(要么成功值要么异常)。恢复一般流程:
- 调用
continuation.intercepted()(由ContinuationInterceptor可替换),得到可能被包装的Continuation。 - 调度(
dispatch)该Continuation的执行(或直接在当前线程执行,取决于 dispatcher)。 - 执行恢复代码块(状态机会根据保存的状态跳转到对应分支)。
- 调用
COROUTINE_SUSPENDED是内部标记,表示当前suspend还未结束,调用者应返回该标记给调用方(编译器在生成代码时使用)。
13. 性能和轻量级的原因
- 协程对象比线程轻很多:创建、切换、内存/上下文开销远小于 OS 线程(协程只是对象与少量堆栈状态)。
- 切换协程并不是线程上下文切换(内核切换),而是把
Runnable入队并在某线程上执行恢复逻辑,代价低。
14. 重要实践与坑(Checklist)
- 记住取消是协作式:在自定义阻塞或长时间计算中要显式检查
isActive或使用yield()。 - 不要把阻塞式 IO 放在 Default:把阻塞IO放
Dispatchers.IO/自定义池。 - 在 finally 中做挂起清理要用 NonCancellable。
- 异常处理:
try/catch保持在launchblock 内;async的异常需要await()才会抛出。 - 使用 suspendCancellableCoroutine 构建可取消挂起,并在
invokeOnCancellation做清理。 - 避免使用 GlobalScope 除非你真的想要全局生命周期。
- 测试:使用
runTest或TestDispatcher控制时间、调度与取消。
15. 常见高级话题(简要导览)
- Select:类似
select/select!,在多个挂起点之间选择第一个完成的(用于竞态)。 - Channels / Actors:基于协程的并发通信原语(类似 Go 的 channel)。
- Flow:基于协程的冷流,支持背压、操作符、终端收集(与 Rx 的 Observable 类比)。
- 协程调试探针(Debug probes):可帮助链路追踪与泄露检测(
DebugProbes.install等工具)。 - Cancellation propagation 优化:
CoroutineStart.LAZY、SupervisorJob、CoroutineScope的不同策略。
16. 例子 — 从头到尾一个完整场景
模拟:发起网络请求,支持取消、超时、并在 finally 中执行清理(使用协程 API):
suspend fun fetchDataWithTimeout(): String = coroutineScope {
val job = launch {
// 监控任务(可选)
}
try {
withTimeout(5_000) {
suspendCancellableCoroutine<String> { cont ->
val call = httpClient.newCall(request)
call.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
cont.resume(response.body!!.string())
}
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
})
cont.invokeOnCancellation {
call.cancel() // 被取消时取消底层请求
}
}
}
} finally {
withContext(NonCancellable) {
// 清理日志、指标上报等
}
}
}17. 总结(要点回顾)
- Kotlin 协程靠编译器将
suspend转为Continuation状态机,从而实现“看似同步、实为异步且不阻塞线程”的效果。 - 挂起点保存状态并返回,恢复通过
Continuation.resumeWith执行;Dispatcher决定恢复在哪个线程执行。 - 取消是协作式:需要使用
suspendCancellableCoroutine、isActive、yield()等来显式支持取消,并在invokeOnCancellation做资源释放。 - 结构化并发(
coroutineScope、SupervisorJob)是 Kotlin 协程的核心设计,保证可控的生命周期与异常语义。 - 异常处理有别于 Rx:流式
onError与协程的try/catch、CancellationException、CoroutineExceptionHandler之间需要区别与配合。