Kotlin协程基础设施篇:函数挂起机制解析
时间:2026-08-21 | 作者:实验室老王 | 阅读:0种一颗树的最好时机是十年前,其次是现在。
学习也一样。
跟着霍老师的《深入理解 Kotlin 携程》学习一下协程。
函数的挂起
协程的挂起和恢复能力,本质上就是函数的挂起和恢复。
在 kotlin 中,使用suspend关键字修饰的函数叫做挂起函数。这种函数只能在协程提或者其他挂起函数中调用。
这样我们就可以把 kotlin 中的函数归为两类:普通函数和挂起函数。
挂起函数不一定真的会被挂起,它只是提供了一个挂起的条件。比如我们可以让挂起函数直接 return。
suspend fun testOne(param:Int):Int{
return param * param
}
这里只是举个例子,这样写的话编辑器会提示我们suspend是个冗余的修饰符。
我们再看另外一个函数。
suspend fun testThree(param: Int) {
val result = suspendCoroutine<Int> { continuation -> println("continuation is $continuation")
continuation.resumeWith(Result.success(param))
}
println("result is $result")
}
可以看到,所谓的协程的挂起,就是程序执行流程发生异步调用时,当前调用流程进入等待状态。
挂起点
对比上面两个函数来看,如果一个函数想要让自己挂起,所需要的无非就是一个 Continuation 实例,那么这个实例怎么来的?
在前面的文章中也提到过,协程体本身也是一个 Continuation 实例。也正式因为这个原因,挂起函数才能在协程体内运行。
在协程内部,挂起函数的调用处被称为挂起点。挂起点如何发生异步调用,那么当前协程就会被挂起,直到对应的 Continuation 的 resume 函数被调用才会恢复执行。
在上面的testThree函数中,从打印结果可以看出获取到的continuation对象是一个SafeContinuation。
continuation is SafeContinuation for Continuation at coroutines.MainKt.testThree
这个类的作用也很简单:确保只有发生异步调用的时候才会挂起。
比如下面的函数就不会挂起。
suspend fun notSuspend() = suspendCoroutine<Int> { continuation -> continuation.resumeWith(Result.success(0)) }
而异步调用是否发生,取决于 resume 函数与对应的挂起函数的调用是否在相同的调用栈上。
CPS 变换
CPS(Continuation-Passing Style,续延传递风格)是一种编程风格。
其核心思想是:函数不直接返回结果,而是接收一个额外的参数,即“续延”(Continuation),并将结果传递给这个续延。
简单来说,续延是一个函数。它代表了一个计算的“未来”或“剩余部分”。它接收一个参数,也就是当前计算的结果,并利用这个参数完成后续所有计算。
举个例子。
直接风格
result = add(1, 2)
print(result)
续延风格
add_cps(1, 2, lambda result: print(result))
在Kotlin的世界里,CPS变换是借助传递Continuation实例来把控异步调用流程的。
当Kotlin协程挂起时,会把挂起点的信息存储在Continuation对象中。该对象承载着协程继续执行所需的上下文信息。等到恢复执行时,只需执行它的恢复操作就可以了。
协程上下文
上下文在许多场景中都有所应用,它本质上是一个概念。像Android、Spring、HarmonyOS等系统或框架中都存在上下文。
通常来讲,上下文具备资源管理、获取配置等功能。
协程的上下文也是如此,不过相较于其他上下文,协程上下文在数据结构方面有着更为突出的特点。
在协程中,我们能够运用操作符+来组合不同类型的协程上下文。
var coroutineContext: CoroutineContext = EmptyCoroutineContext
coroutineContext += CoroutineName("huangyuan-01")
coroutineContext += CoroutineName("huangyuan-02")
coroutineContext += CoroutineExceptionHandler { context, ex -> println("CoroutineExceptionHandler got $ex") }
suspend {
println("In Coroutine [${coroutineContext[CoroutineName]} ].")
println("In Coroutine [${coroutineContext[CoroutineExceptionHandler]} ].")
5
}.startCoroutine(object : Continuation<Int> {
override val context = coroutineContext
override fun resumeWith(result: Result<Int>) {
result.onFailure {
context[CoroutineExceptionHandler].handleException(context, it)
}.onSuccess {
println("Coroutine [${coroutineContext[CoroutineName]}] completed success")
println("Coroutine [${coroutineContext[CoroutineExceptionHandler]}] completed success")
}
}
})
我们可以看到打印的日志。
In Coroutine [CoroutineName(huangyuan-02) ].
In Coroutine [coroutines.MainKtmain$$inlinedCoroutineExceptionHandler$1@4f933fd1 ].
Coroutine [CoroutineName(huangyuan-02)] completed success
Coroutine [coroutines.MainKtmain$$inlinedCoroutineExceptionHandler$1@4f933fd1] completed success
合并规则
- 非交换性:ctx1 + ctx2 ≠ ctx2 + ctx1(顺序很重要)
- 结合性:(ctx1 + ctx2) + ctx3 = ctx1 + (ctx2 + ctx3)
- 键唯一性:相同 Key 的元素会被覆盖
注意,这里的 Key 定义是
public interface Key<E : Element>
public interface Element : CoroutineContext {
public val key: Key<*>
}
CombinedContext
注意这里还有一个比较重要的类,后面重写操作符的时候会用到。
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable {
override fun get(key: Key<E>): E {
var cur = this
while (true) {
cur.element[key].let { return it }
val next = cur.left
if (next is CombinedContext) {
cur = next
} else {
return next[key]
}
}
}
}
示例
规则 1:相同 Key 的元素,右边的覆盖左边的
val context1 = Job() + CoroutineName("First")
val context2 = CoroutineName("Second") + Dispatchers.IO
val result = context1 + context2
// 结果包含:Job(来自context1), Dispatchers.IO(来自context2), CoroutineName("Second")(来自context2,覆盖了context1的)
规则 2:EmptyCoroutineContext 是中性元素
val context = Dispatchers.IO + CoroutineName("Test")
val result1 = context + EmptyCoroutineContext // 等于 context
val result2 = EmptyCoroutineContext + context // 等于 context
规则 3:组合是通过链表实现的
上下文组合实际上形成了一个链表结构,每个节点包含自己的元素并指向下一个上下文。
源码分析
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- a void lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
这么看源码就比较简单了。
- 第一步:快速路径优化:如果合并的上下文是空的,直接返回当前上下文,避免创建 lambda 和执行 fold 操作。
- 第二步:这里使用了 fold 操作:
- 初始值:this(当前上下文)
- 遍历:context 中的每个 element
- 操作:对于每个元素,从累积值 acc 中移除相同 Key 的元素,然后处理合并
- 第三步:强制将拦截器放在尾部,因为在协程执行过程中,获取调度器(ContinuationInterceptor)是一个非常频繁的操作。每次协程恢复执行时都需要检查当前的调度器,现在通过强制让拦截器位于链尾,使得查找可以在 O(1) 时间内完成。
这样我们就了解了CoroutineContext.plus这个 Kotlin 协程上下文系统的核心函数。
理解这个方法的工作原理,对于编写高效、可维护的协程代码至关重要。
特别是在需要精细控制协程执行环境的场景中,更是如此。
以上
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- GitLab重组裁员14%并重构Git基础设施应对AI百倍负载
- 时间:2026-08-21
-
- 中国机器人供应链转向基础设施赛道的现场观察
- 时间:2026-08-17
-
- 三星会长李在镕会见奥特曼,AI基础设施合作成焦点
- 时间:2026-08-15
-
- 微软启用印度第四个云区域加码AI基础设施建设
- 时间:2026-08-12
-
- 跨境电商智能基础设施三层架构的云原生大数据AI实践
- 时间:2026-07-28
精选合集
更多大家都在玩
大家都在看
更多-
- 糖尿病完全不能吃糖吗
- 时间:2026-09-15
-
- 蚂蚁庄园小课堂2026年9月16日最新题目答案
- 时间:2026-09-15
-
- 小鸡答题今天的答案是什么2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园每日答题答案2026年9月16日
- 时间:2026-09-15
-
- 以下哪种粮食是酿造绍兴黄酒的主要原料 蚂蚁庄园今日答案9月16日
- 时间:2026-09-15
-
- 劝学名句“及时当勉励,岁月不待人”出自哪位诗人 蚂蚁庄园今日答案9.16
- 时间:2026-09-15
-
- 蚂蚁庄园今天答题答案2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园答题今日答案2026年9月16日
- 时间:2026-09-15