Skip to content

字节码、安全恢复与原语

源:Kotlin Coroutines: Design and Implementation

这一章节涉及协程运行时最底层的“黑盒”操作:编译器如何处理挂起点,以及运行时如何确保线程安全地恢复执行。

挂起的标志

当一个挂起函数执行时,它会返回一个特殊的值 COROUTINE_SUSPENDED

  • 如果函数立即返回结果(未挂起),它直接返回数据对象。
  • 如果函数真正挂起了,它必须返回单例标志 kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED

为什么需要这个标志?

这告诉状态机(Continuation):当前的线程执行已经结束,请保存状态并退出,不要进入下一个 case。

安全恢复:SafeContinuation

在异步回调中,如果回调触发得太快(甚至在挂起函数还没返回 COROUTINE_SUSPENDED 之前就调用了 resume),会导致状态机错乱。

核心逻辑

SafeContinuation 通过一个状态原子变量来解决这个问题:

  1. 先 resume 再 return:它会将结果保存在内部,当挂起函数返回时,发现结果已就绪,则直接返回结果而非挂起标志。
  2. 先 return 再 resume:它会正常返回挂起标志,后续的 resume 会按照标准流程触发状态机。

字节码溢出 (Spilling)

在 CPS 变换过程中,由于状态机对象是在堆上分配的,局部变量必须从栈“溢出”到堆上的字段中,以便在恢复时重新加载。

溢出示例 (伪字节码)
// 原代码
val x = compute()
suspendCall()
print(x)

// 变换后
this.x = compute(); // 将 x 保存到 Continuation 字段
suspendCall(this);
// 恢复后
val x = this.x;    // 从字段恢复到局部变量
print(x);

底层原语 (Intrinsics)

Kotlin 暴露了一些以 suspend 开头的内联原语,它们不经过正常的 CPS 变换,用于构建基础库:

  • suspendCoroutineUninterceptedOrReturn: 核心原语,允许访问当前的续体并决定是挂起还是立即返回。
  • intercepted(): 用于获取经过拦截器(调度器)处理后的续体。

性能优化点:内联与尾递归

  1. 尾递归挂起函数:如果挂起函数在最后一行调用另一个挂起函数,编译器会进行优化,减少一层 Continuation 的包装。
  2. 内联函数中的 suspend:由于内联在编译期展开,它不会引入额外的类,但会增加状态机生成的复杂性。

核心准则总结

  1. 不要手动返回 COROUTINE_SUSPENDED:除非你在编写极低层的库代码。
  2. 理解变量溢出对内存的影响:在长耗时挂起点前,尽量手动将不再需要的大对象置为 null。
  3. 信任编译器优化:现代 Kotlin 编译器已经能处理绝大部分性能边界情况。