位置:首页 > Kotlin > Kotlin协程基础设施篇:函数挂起机制解析

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 协程上下文系统的核心函数。

理解这个方法的工作原理,对于编写高效、可维护的协程代码至关重要。

特别是在需要精细控制协程执行环境的场景中,更是如此。

以上

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多