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 协程上下文系统的核心函数,理解这个方法的工作原理对于编写高效、可维护的协程代码至关重要,特别是在需要精细控制协程执行环境的场景中。


以上

相关推荐
Kapaseker18 分钟前
一杯美式搞定 Kotlin 空安全
android·kotlin
FunnySaltyFish18 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
Kapaseker1 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
Kapaseker2 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z4 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton4 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream5 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam5 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker5 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
糖猫猫cc6 天前
Kite:两种方式实现动态表名
java·kotlin·orm·kite