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
保持在launch
block 内;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
之间需要区别与配合。