kotlin协程-基础设施篇-函数的挂起

种一颗树的最好时机是十年前,其次是现在。 学习也一样。 跟着霍老师的《深入理解 Kotlin 携程》学习一下协程。

函数的挂起

协程的挂起和恢复能力本质上就是函数的挂起和恢复。在 kotlin 中,使用suspend关键字修饰的函数叫做挂起函数,这种函数只能在协程提或者其他挂起函数中调用。这样我们就可以把 kotlin 中的函数归为两类:普通函数和挂起函数。 挂起函数不一定真的会被挂起,它只是提供了一个挂起的条件。比如我们可以让挂起函数直接 return。

Kotlin 复制代码
suspend fun testOne(param:Int):Int{
    return  param * param
}

这里只是举个例子,这样写的话编辑器会提示我们suspend是个冗余的修饰符。

我们再看另外一个函数

Kotlin 复制代码
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

这个类的作用也很简单:确保只有发生异步调用的时候才会挂起。 比如下面的函数就不会挂起

Kotlin 复制代码
suspend fun notSuspend() = suspendCoroutine<Int> { continuation -> continuation.resumeWith(Result.success(0)) }

而异步调用是否发生取决于 resume 函数与对应的挂起函数的调用是否在相同的调用栈上。

CPS 变换

CPS(Continuation-Passing Style,续延传递风格)是一种编程风格,其核心思想是:函数不直接返回结果,而是接收一个额外的参数,即"续延"(Continuation),并将结果传递给这个续延。 简单来说,续延是一个函数,它代表了一个计算的"未来"或"剩余部分"。它接收一个参数(即当前计算的结果),并利用这个参数来完成后续所有的计算。 举个例子

直接风格

kotlin 复制代码
result = add(1, 2)
print(result)

续延风格

kotlin 复制代码
add_cps(1, 2, lambda result: print(result))

在 kotlin 里面,CPS 变换是通过传递 Continuation 实例来控制异步调用流程的。Kotlin 协程挂起的时候,就是将挂起点的信息保存在了 Continuation 对象中,它携带了协程继续执行时所需要的上下文信息,在恢复执行时,只需要执行它的恢复即可。

协程上下文

上下文在很多地方都有它的身影,它只是一个概念,比如 Android 中的上下文,Spring 中的上下文,HarmonyOS 中的上下文。一般情况下,上下文承载了资源管理、获取配置等功能。 那么协程的上下文也是这样,只不过相比于其他的上下文,协程上下文有更显著的数据结构特征。 我们可以使用操作符+来对不同类型的协程上下文进行组装,

kotlin 复制代码
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 定义是

Kotlin 复制代码
public interface Key<E : Element>
public interface Element : CoroutineContext {
  public val key: Key<*>
}

CombinedContext

注意这里还有一个比较重要的类,后面重写操作符的时候会用到

Kotlin 复制代码
internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
    override fun <E : Element> 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 的元素,右边的覆盖左边的

Kotlin 复制代码
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 是中性元素

Kotlin 复制代码
val context = Dispatchers.IO + CoroutineName("Test")
val result1 = context + EmptyCoroutineContext  // 等于 context
val result2 = EmptyCoroutineContext + context  // 等于 context

规则 3:组合是通过链表实现的 上下文组合实际上形成了一个链表结构,每个节点包含自己的元素并指向下一个上下文。

源码分析

Kotlin 复制代码
public operator fun plus(context: CoroutineContext): CoroutineContext =
    if (context === EmptyCoroutineContext) this else // fast path -- avoid 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 协程上下文系统的核心函数,理解这个方法的工作原理对于编写高效、可维护的协程代码至关重要,特别是在需要精细控制协程执行环境的场景中。


以上

相关推荐
Kapaseker14 小时前
你不看会后悔的2025年终总结
android·kotlin
alexhilton17 小时前
务实的模块化:连接模块(wiring modules)的妙用
android·kotlin·android jetpack
幽络源小助理21 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
QING6181 天前
SupervisorJob子协程异常处理机制 —— 新手指南
android·kotlin·android jetpack
W个世界1 天前
06-区间与迭代
kotlin
Fate_I_C1 天前
Kotlin 中的 suspend(挂起函数)
android·开发语言·kotlin
凡小烦1 天前
看完你就是古希腊掌管Compose输入框的神!!!
android·kotlin
モンキー・D・小菜鸡儿1 天前
kotlin 斗牛小游戏
kotlin·小游戏