Kotlin 协程源代码泛读:suspend 函数

使用 Kotlin 协程绕不开 suspend(挂起)函数,那么 suspend 函数的本质是什么?

在这篇文章中我尝试讲讲我所理解的 suspend 函数

我们仍然可以通过反编译 Kotlin 代码来窥探 suspend 函数的细节

kotlin 复制代码
fun main() {
    runBlocking {
        doSuspend()
    }
}

suspend fun doSuspend() {
    delay(1000L)
    println("doSuspend")
}

这段简单的代码反编译成 Java 代码后是这个样子的

less 复制代码
@Nullable
public static final Object doSuspend(@NotNull Continuation $completion) {
   ...
}

emm, 具体的实现我们先不看,因为信息量有点多...,容易被劝退

首先,我们可以确认的是 suspend 函数也是一个普通的函数

其次,它的内部实现被 Kotlin 编译器插件动过手脚...,帮我们生成了很多样板代码

再次,编译器插入了一个类型为 Continuation 的 completion 参数,这个 Continuation 在之前的系列文章提及过,可以想象成是对状态机(State Machine)的封装,而(suspend)函数可以认为是一个状态机

参数的命名也是有讲究的,completion,即完成的意思,最终执行结果通过它回调出去

你可以联想一下在「史前」时代我们如何封装一个异步方法

kotlin 复制代码
fun doSuspend(callback: Callback) {
   ...
}

给函数增加一个 callback,以便在它干完事后通知一下我们,所以你也可以将 Continuation 和 Callback 进行类比,降低接收新事物的难度

最后总结一下:suspend 函数是一个带有 completion (回调) 尾部参数的函数

基于这个理解,我们可以写出下面这个搞怪的代码,当然只是为了验证我们的推断

kotlin 复制代码
fun main() {
    (::doSuspendMock as (suspend () -> Unit)).startCoroutine(
        Continuation(
            context = EmptyCoroutineContext,
            resumeWith = {
                println("ok, get!")
            }
        )
    )
}

fun doSuspendMock(completion: Continuation<Unit>) {
    println("doSuspendMock")
}

doSuspendMock 并不是 suspend 函数(没有 suspend 关键字),我可以把它强制转换成 suspend fun

startCoroutine 是 Kotlin 协程框架提供的方法,用于执行一个 suspend 方法(启动协程),这里我们手动创建了一个 Continuation(回调)用于接收 suspend 方法执行结果:

arduino 复制代码
doSuspendMock
ok, get!

ok,我们接着看看之前说的信息量比较大的部分...

这段代码初见不好理解,需要你把自己想象成一台「电脑」,然后沉浸式的体验一下执行流程

任何的技术文章的解读都不及你自己静下心来在脑袋里跑一遍,所以我直接在代码上做点关键性的注释

php 复制代码
   @Nullable
   public static final Object doSuspend(@NotNull Continuation $completion) {
      Continuation $continuation;
      // 这个便签内的代码用于创建和维护 Continuation(Callback)
      label20: {
         if ($completion instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)$completion;
            if (($continuation.label & Integer.MIN_VALUE) != 0) {
               $continuation.label -= Integer.MIN_VALUE;
               break label20;
            }
         }
         // 首次执行时,上面那个 if 条件不满足,所以会创建一个 Continuation 对象
         // 它会贯穿整个 suspend 函数的生命周期
         // 想想为什么这里需要一个 Continuation?为了接收 delay 的回调
         // delay 也是一个 suspend 函数,在它执行完之后通知我们继续执行...
         $continuation = new ContinuationImpl($completion) {
            // $FF: synthetic field
            Object result;
            // 你可以把这个 label 想象成状态机的状态,初始值为 0
            int label;
            // 你可以把这个 invokeSuspend 函数想象成状态机在执行状态迁移 
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               // 所谓的状态转移,就是重复的调用自个儿 doSuspend
               return SuspendKt.doSuspend((Continuation)this);
            }
         };
      }
      // 这个 switch 就是所谓的状态机,因为我们只调用了一个 suspend 函数(delay)
      // 分支会比较少,同时结构也比较简单
      Object $result = $continuation.result;
      Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch ($continuation.label) {
         case 0:
            ResultKt.throwOnFailure($result);
            // 将状态设置 1,下次回调 doSuspend 的时候,走 case 1
            $continuation.label = 1;
            // delay 函数也是一个 suspend 函数,continuation 用于接收回调
            // 这里是实现挂起的细节,可以细品
            if (DelayKt.delay(1000L, $continuation) == var3) {
               return var3;
            }
            break;
         case 1:
            ResultKt.throwOnFailure($result);
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }
      // ok,我们的代码终于出现了
      System.out.println("doSuspend");
      return Unit.INSTANCE;
   }

总结 suspend 函数由两部分组成,无论多复杂都是如此:

  • Continuation(回调)初始化
  • 状态机

suspend 函数内部如果调用别的 suspend 函数,它们会将 caller「分割」成多个状态

suspend 函数的执行就是在状态机内部打转,然后通过 completion 回调出去

顺便提一句谨慎使用 suspend,Kotlin 编译器插件夹带的私货有点多

Android Studio/Intellij IDE 中如果你声明的 suspend 函数并不会挂起它会建议你删除 suspend 关键字!

相关推荐
Z_W_H_2 分钟前
【Springboot】Bean解释
java·开发语言
Otaku love travel1 小时前
老系统改造增加初始化,自动化数据源配置(tomcat+jsp+springmvc)
java·tomcat·初始化·动态数据源
DKPT1 小时前
Java设计模式之行为型模式(责任链模式)介绍与说明
java·笔记·学习·观察者模式·设计模式
L_autinue_Star1 小时前
手写vector容器:C++模板实战指南(从0到1掌握泛型编程)
java·c语言·开发语言·c++·学习·stl
晨岳2 小时前
CentOS 安装 JDK+ NGINX+ Tomcat + Redis + MySQL搭建项目环境
java·redis·mysql·nginx·centos·tomcat
执笔诉情殇〆2 小时前
前后端分离(java) 和 Nginx在服务器上的完整部署方案(redis、minio)
java·服务器·redis·nginx·minio
YuTaoShao2 小时前
【LeetCode 热题 100】24. 两两交换链表中的节点——(解法一)迭代+哨兵
java·算法·leetcode·链表
程序员的世界你不懂2 小时前
(20)Java+Playwright自动化测试- 操作鼠标拖拽 - 上篇
java·python·计算机外设
AI360labs_atyun2 小时前
Java在AI时代的演进与应用:一个务实的视角
java·开发语言·人工智能·科技·学习·ai
不像程序员的程序媛3 小时前
redis的一些疑问
java·redis·mybatis