核心总览
- Continuation:每个 suspend 在编译后都会多一个 Continuation,恢复时通过 resumeWith 继续执行“状态机”。
- ContinuationInterceptor:协程上下文里的一个元素,拦截并包装原始 Continuation,决定在哪个线程/队列恢复(即“调度”)。
- DispatchedContinuation:kotlinx-coroutines 内部对 Continuation 的包装,持有 dispatcher + context,在 resumeWith 时选择直接执行还是投递到队列。
1) ContinuationInterceptor:怎么“拦截”与“调度”
ContinuationInterceptor 是一个 CoroutineContext.Element(Key 唯一),协程启动/挂起点创建 continuation 时,会调用:
val intercepted: Continuation<T> = interceptor.interceptContinuation(original)
你拿到的是一个被拦截后的 continuation(通常是 DispatchedContinuation)。其关键职责:
-
选择线程/队列:
- 通过 dispatcher.isDispatchNeeded(context) 判断是否需要切换;
- 若需要,dispatcher.dispatch(context, Runnable) 把恢复任务投递到目标线程/事件循环。
-
恢复语义:
-
resumeWith 与 resumeCancellableWith(可取消恢复)统一走拦截器;
-
控制 inline 恢复(undispatched) 或 异步派发 的策略。
-
上下文里只能有一个 ContinuationInterceptor(它通常就是 Dispatchers.Default/IO/Main 等之一)。
2) DispatchedContinuation:恢复路径怎么走
内部要点(概念化):
override fun resumeWith(result: Result<T>) {
if (dispatcher.isDispatchNeeded(context)) {
// 需要切线程/排队
saveState(result)
dispatcher.dispatch(context, thisAsRunnable)
} else {
// 同线程可直接执行(undispatched/inline fast-path)
runUndispatched(result) // 期间会处理 ThreadContextElement 的安装/还原
}
}
- fast-path:当前就在目标“事件循环/线程”,走同线程 inline 恢复(减少切换和排队)。
- dispatch-path:不在目标线程,就打包成任务投递给 dispatcher(如主线程 Looper、Default 池)。
- 事件循环协作:为避免深度递归/栈爆,内部有轻量 event loop 控制(yield/Unconfined 时尤其重要)。
3) 常见 Dispatcher 的调度语义
-
Dispatchers.Default:CPU 密集型线程池(与 IO 共享底池但有并行度限制);
-
Dispatchers.IO:面向阻塞 I/O,更高并行度的视图(避免 I/O 堵住 CPU 池);
-
Dispatchers.Main:Android 主线程(Looper);
-
Dispatchers.Main.immediate:若当前就在主线程且处于正确事件循环,isDispatchNeeded=false,直接 inline 恢复;否则投递到主线程队列。
-
Dispatchers.Unconfined:不强制切换到任何固定线程:
- 首次恢复在当前线程 inline 执行;
- 发生挂起后,下一次恢复在哪个线程由真正唤醒你的挂起点决定(可能是 I/O 回调线程、定时器线程……)。
- 只适合非常底层、明白其时序者;UI/业务逻辑中少用。
4)withContext的线程切换与恢复
suspend fun <T> withContext(ctx: CoroutineContext, block: suspend () -> T): T
-
进入时:若 ctx 的拦截器不同,创建新的 DispatchedContinuation 并切换到目标调度器。
-
执行 block;期间遇到挂起点,保存现场并返回 COROUTINE_SUSPENDED。
-
恢复时:由目标调度器决定在哪个线程执行余下代码。
-
退出 withContext 时:还原到调用者上下文(包括线程局部,见下)。
优化:当 withContext(Dispatchers.Main.immediate) 且正处于主线程时,通常不派发,直接 inline(避免多余切换)。
5) 线程局部(ThreadLocal)如何随协程一起“切”
协程不总是回到同一线程,ThreadLocal 会丢失。为此有:
-
ThreadContextElement 接口:定义 updateThreadContext / restoreThreadContext。
- 常见实现:kotlinx-coroutines 的 ThreadLocal.asContextElement()、SLF4J MDC 适配、Transaction/Tracing 等。
-
拦截器在恢复前安装这些上下文,恢复后再还原,保证“看起来像在一个连续的线程上下文里”。
6) Start 模式与调度:DEFAULT / LAZY / ATOMIC / UNDISPATCHED
- CoroutineStart.DEFAULT:立即调度;若可 inline(如 Main.immediate)则直接执行。
- LAZY:直到 start/join/await 才启动。
- ATOMIC:启动不可取消直到第一次挂起点(避免“刚启动就被取消”)。
- UNDISPATCHED:在当前线程直接运行到首个挂起点,然后再交给目标调度器继续(减少一次派发)。
7)yield()、公平性与事件循环
- yield():把自己重新排队到 dispatcher,给同队列的其他任务一个执行机会(避免“独占”)。
- 某些 dispatcher(如主线程)通过事件循环保证“可重入调用”不至于无限递归(Unconfined/immediate 情况尤需)。
8) 取消、异常与调度的协作
- 取消是通过在下一次恢复时注入 CancellationException 完成的;
- resumeCancellableWith 会在恢复前检查 Job 状态;已取消则不再执行用户代码,而是走异常路径;
- try/finally 被编译进状态机,保证 finally 必达(无论是正常返回、异常还是取消)。
9) 调试与“栈追踪恢复”
- 因为是 stackless,原生调用栈很浅;库通过 Stacktrace Recovery 把挂起点链路拼回堆栈,便于定位。
- 调试探针(DebugProbes)与日志上下文常借助拦截/线程上下文机制插入信息(协程 id、线程名等)。
10) 常见坑与实践
坑
-
Unconfined 线程漂移:一次挂起后,恢复线程不可预测;不要触碰 UI/线程局部资源。
-
Main.immediate 重入:在主线程内层层 withContext(Main.immediate) 可能形成同步重入,注意可重入性/锁。
-
在锁内挂起:持有 synchronized/互斥锁期间 await/delay 会导致长时间持锁或死锁;把挂起点挪到锁外。
-
误以为 withContext(IO) 就一定立刻切线程:isDispatchNeeded 可能为 false(如已在同一池且允许 inline),所以不要依赖“切线程”作为内存可见性屏障,用 withContext 的语义保证即可。
-
ThreadLocal 泄漏/错位:忘了用 asContextElement、自定义 ThreadContextElement,会出现上下文丢失。
实践
- UI:默认用 Main,IO/CPU 分别在 IO/Default;必要时用 Main.immediate 减少切换;
- 底层库/桥接回调:可用 Unconfined/UNDISPATCHED 优化首段执行,但不要把它们“冒泡”到业务层;
- 需要限流:Dispatcher.limitedParallelism(n) 为局部并行度设上限;
- 频繁切换热路径:合并 withContext,或通过“批处理 + 单次切换”降低派发成本;
- 线程局部:ThreadLocal.asContextElement(value) 或实现 ThreadContextElement。
迷你速查表
-
拦截谁? ContinuationInterceptor 拦截并包装 Continuation。
-
谁调度? dispatcher.isDispatchNeeded 决定 inline 还是 dispatch。
-
主线程立刻跑? Main.immediate 在主线程可 inline。
-
线程不固定? Unconfined 首次当前线程、其后由唤醒源决定。
-
跨线程携带 ThreadLocal? 用 ThreadContextElement(如 asContextElement)。
-
如何少一次切换? UNDISPATCHED 或 Main.immediate。
-
公平让路? yield()。