Kotlin协程的JVM实现源码分析(下)

协程 根据 是否保存切换 调用栈 ,分为:

  1. 有栈协程(stackful coroutine)
  2. 无栈协程(stackless coroutine)

在代码上的区别是:是否可在普通函数里调用,并暂停其执行。

Kotlin协程,必须在挂起函数中调用和恢复,属于 无栈协程

常见的语言,协程实现:

  • 有栈协程:Go、Lua
  • 无栈协程:Kotlin、C++ 20、Clojure、JavaScript

二、无栈协程 和 Continuation

2.1 CPS(Continuation-passing-style)

在上篇源码分析中,不难发现 执行的结果,都是通过 Continuation 来返回。

2.1.1 Continuation

Continuation 就是 一个通用的回调接口,返回 Result<T> 值 或 异常。

Continuation is a generic callback interface. ------ Roman Elizarov

kotlin 复制代码
public interface Continuation<in T> {

    public val context: CoroutineContext

    public fun resumeWith(result: Result<T>)
}
2.1.2 CPS

挂起函数 调用 其他挂起函数时,会将自己的 Continuation对象 作为 completion 参数 传递,

这种传递Continuation的方式,称为 连续传递风格(Continuation-passing-style) ,简称为 CPS

挂起函数 编译后,会创建基于 ContinuationImpl 对象,把 调用者Continuation 传给 completion 构造参数:

kotlin 复制代码
internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
)
2.1.3 Continuation结果返回

上篇知道 协程执行在 BaseContinuationImpl.resumeWith 方法,

同样 结果返回逻辑 也在这里,看下代码:

和 传递逻辑顺序 相反,结果按 逐步向上 返回。

分析:当获取结果后,通过 while 循环,completion 将结果向上传递,一般是协程 StandaloneCoroutine 作为最终的 completion 完成结果回调。

2.2 状态机

无栈协程,是通过 状态机状态 保存恢复 来实现协程挂起恢复。
和 每个 回调 都要创建 回调对象 相比,状态机 通过 状态 记录 执行位置,

当 挂起函数完成后,只需 恢复状态 接着执行后面的代码。

其实就是通过 switch(label) 做判断,判断位置执行。

状态机 vs 回调,有以下几个优点:

  1. 复用 方法对象和状态,避免每次分配对象
  2. 简化 循环 和 使用 高阶函数

以下面 请求解析数据 为例,launch {} 对应的 lambda挂起函数 ,分析 Kotlin 状态机状态:

kotlin 复制代码
GlobalScope.launch {
  // 挂起点1
  val data = getData()
  // 挂起点2
  val result = parseData(data)
  println("data: $data, result: $result")
}

Kotlin编译后逻辑,以 伪代码 表示:

kotlin 复制代码
class $main$1 extends SuspendLambda {
  // 挂起点的位置
  int label;
  // 状态 对象 保存 和 恢复
  Object L$0;
  // 更多状态: L$1 L$2 ...

  Object invokeSuspend(Object result) {
    Object obj;
    switch (this.label) {
      case 0:
        this.label = 1;
        obj = getData(this);
        // 表示挂起,存储 状态 label = 1,
        // 恢复时再次调用 invokeSuspend,恢复执行下面
        if (obj == COROUTINE_SUSPENDED) {
          return COROUTINE_SUSPENDED;
        }
        // 没有break,如果没有挂起,直接 执行下面的过程

      case 1:
		// 挂起恢复后
		String data = (String) result;
		// 如果没有挂起,直接执行则是:
		// String data = (String) obj;
        this.label = 2;
        // 保存 状态
        this.L$0 = data;
        obj = parseData(data, this);
        if (obj == COROUTINE_SUSPENDED) {
          return COROUTINE_SUSPENDED;
        }

      case 2:
		// 挂起恢复后
		Integer num = (Integer) result;
		// 如果没有挂起,直接执行则是:
		// Integer num = (Integer) obj;
		// 恢复状态
        String data = (String) this.L$0;
        System.out.println("data: " + data + ",num: " + num);
        return Unit.INSTANCE;

      default:
        throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
    }
    
  }
}

2.3 CPS Transform

上面说到调用挂起函数 continuation 会作为函数参数传递,但是 声明挂起函数时,

并没有 continuation参数。而是 Kotlin 会在参数列表 自动加上 Continuation 参数,这个操作叫做 CPS Transform

举例,下面挂起函数:

kotlin 复制代码
suspend fun <T> CompletableFuture<T>.await(): T

而在 CPS Transform 后,实际的代码是:

kotlin 复制代码
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?

小结

  • Kotlin协程,通过 状态机 实现,复用闭包。
  • 挂起函数, 编译成 Continuation 回调对象,CPS。
  • suspend 以同步的编程方式,执行异步方法

文档

相关推荐
白露与泡影27 分钟前
Spring Boot项目优化和JVM调优
jvm·spring boot·后端
Boop_wu2 小时前
[Java EE] 多线程 -- 初阶(2)
java·开发语言·jvm
Chan164 小时前
【 Java八股文面试 | JVM篇 内存结构、类加载、垃圾回收与性能调优 】
java·jvm·spring boot·后端·spring·idea
百***926510 小时前
java进阶1——JVM
java·开发语言·jvm
虫师c10 小时前
字节码(Bytecode)深度解析:跨平台运行的魔法基石
java·jvm·java虚拟机·跨平台·字节码
坐吃山猪1 天前
第2章-类加载子系统-知识补充
jvm
安冬的码畜日常1 天前
【JUnit实战3_33】第二十章:用 JUnit 5 进行测试驱动开发(TDD)(下)——TDD 项目的重构过程及新功能的开发实战
测试工具·junit·单元测试·测试驱动开发·tdd·junit5·test-driven
百***92021 天前
java进阶1——JVM
java·开发语言·jvm
Pluchon1 天前
硅基计划6.0 柒 JavaEE 浅谈JVM&GC垃圾回收
java·jvm·数据结构·java-ee·gc
zhangphil1 天前
Kotlin协程Flow流buffer缓冲批量任务或数据,条件筛选任务或数据
kotlin