Kotlin协程挂起恢复源码解读

42 阅读5分钟

背景

Kotlin协程,在使用已经很多了,对于其挂起和恢复的原理却没有深入地分析过。只了解到有一个CPS转换,将协程里的suspend方法,分割成了一个个的Continuation续体对象,然后通过回调的方式来进行恢复通知。

得空计划写一个简单的调用流程,反编译为Java代码,然后从入口处,一点点分析完整的挂起和恢复流程。测试环境为桌面端CMP项目内,版本是Kotlin2.1.0.

CPS转换

在Kotlin协程中,挂起函数的执行是通过 Continuation Passing Style (CPS)转换 来实现的。CPS转换是一种将函数式编程中的函数调用转换为可传递的 Continuation 对象的过程。这里的转换是Kotlin编译器实现的,在跨平台属性上,也保证了流程的一致性。

Kotlin协程通过将异步流程拆解为一系列 挂起点 ,对含有 suspend 关键字的函数进行了 CPS转换 ,即Continuation Passing Style转换,使其能够 接收Continuation对象 作为参数,并在异步操作完成后通过调用 Continuation 的恢复方法来继续执行协程。

在编译后的字节码中,协程的状态会被转换为状态机的形式,每个挂起点对应状态机的一个状态。当协程挂起时,它的执行状态会被保存在Continuation对象中,包括局部变量上下文和执行位置。

Continuation

Continuation (续体)是一个保存协程状态的对象,它记录了协程挂起的位置以及局部变量上下文,使得协程可以在任何时候从上次挂起的地方继续执行。Continuation是一个接口,它定义了 resumeWith 方法,用于恢复协程的执行。

interface Continuation<in T> {
   val context: CoroutineContext
   fun resumeWith(result: Result<T>)//result 为返回的结果
}
  1. 续体是一个较为抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成一个又一个续体。
  2. 在suspend函数或者 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行suspend函数或者await 函数后面的代码。

CPS转换 使得协程能够在不阻塞线程的情况下执行异步操作。当协程挂起时,线程可以被释放去执行其他任务,从而提高了系统的并发性能。此外,CPS转换使得协程的挂起和恢复操作对开发者来说是透明的,开发者可以像编写同步代码一样编写异步代码。

发生 CPS 变换的函数,返回值类型变成了 Any?,这是因为这个函数在发生变换后,除了要返回它本身的返回值,还要返回一个标记CoroutineSingletons.COROUTINE_SUSPENDED,为了适配各种可能性,CPS 转换后的函数返回值类型就只能是 Any?了。

源码分析

编写的Kotlin测试代码如下:

class MySimpleTest {

    suspend fun stephenTest(): String {
        delay(500L)
        return "result From stephenTest"
    }
}

fun callFromOutside() {
    CoroutineScope(Dispatchers.IO).launch {
        val result = MySimpleTest().stephenTest()
        println(result)
    }
}

在callFromOutside函数中,我们创建了一个协程作用域,并在其中启动了一个协程。该协程将调用stephenTest函数。而stephenTest函数是一个挂起函数,它会暂停该协程的执行,直到delay函数返回。

将这个片段反编译成java代码,删掉导包和元数据注解信息等,详细的分析过程直接见注释流程编号:

public final class MySimpleTest {
   public static final int $stable;

   @Nullable
   // (8)stephenTest函数本来是无参的,现在有一个Continuation类型的参数
   // 这个就是外部调用代码块封装成的实例,stephenTest方法执行完毕,需要继续往下执行的代码都在这个对象里面
   public final Object stephenTest(@NotNull Continuation $completion) {
      Continuation $continuation;
      // (9)label20: 是一个Java中的标签(label),主要用于控制流程跳转。在这里它被用来实现协程的挂起和恢复机制
      label20: {
         if ($completion instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)$completion;
            // (10)用于检查当前协程是否处于挂起状态。Integer.MIN_VALUE 是一个特殊的标志位,用于标记协程是否被挂起。
            // 它的值是10000000 00000000 00000000 00000000
            // label首次传进来是1,即00000000 00000000 00000000 00000001,和Integer.MIN_VALUE按位与的结果为0,表示需要挂起,会走到11步,基于外部传入的 completion 对象创建一个新的ContinuationImpl对象
            //=======================分割线====================
            // (16)这里的label在15步被赋值成了10000000 00000000 00000000 00000001,按位与的结果是Integer.MIN_VALUE,即条件检查结果为真(即 != 0)
            // 10000000 00000000 00000000 00000001减去Integer.MIN_VALUE,结果是1,即00000000 00000000 00000000 00000001
            // 并将label20标签跳出循环,继续往下执行stephenTest的switch状态判断
            if (($continuation.label & Integer.MIN_VALUE) != 0) {
               $continuation.label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         // (11)开始创建关于stephenTest代码块的ContinuationImpl对象,用于传递给下一个suspend函数
         $continuation = new ContinuationImpl($completion) {
            // $FF: synthetic field
            Object result;
            // 初始值为0
            int label;

            @Nullable
            // (13)delay执行完,调用resumeWith,触发这个invokeSuspend方法
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               //(14)将label = 1和Integer.MIN_VALUE按位或,
               // 运算的结果是 10000000 00000000 00000000 00000001(即 -2147483647)
               this.label |= Integer.MIN_VALUE;
               //(15)重入调用stephenTest函数,这次是传入 $continuation 自己作为参数。
               return MySimpleTest.this.stephenTest((Continuation)this);
            }
         };
      }


      Object $result = $continuation.result;
      Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      // (17) 本轮调用中,label值为1,检查无异常后,就会返回这个字符串
      // "result From stephenTest"
      switch ($continuation.label) {
         case 0:
            ResultKt.throwOnFailure($result);
            $continuation.label = 1;
            // (12)调用delay函数,之后就和外部调用的(3)-(7)步流程一样.
            // 传入ContinuationImpl对象,delay函数内部会判断是否需要挂起,如果需要挂起,就return掉本轮stephenTest方法的调用
            // 进入了delay内部执行,等500ms过后,调用外部传进来的ContinuationImpl对象的 resumeWith 函数回调
            // 而resumeWith方法,必然会调用到这个ContinuationImpl 对象自己的invokeSuspend方法,就跳转到第13步了
            if (DelayKt.delay(500L, $continuation) == var4) {
               return var4;
            }
            break;
         case 1:
            ResultKt.throwOnFailure($result);
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      return "result From stephenTest";
   }
}

// CoroutineTestKt.java
public final class CoroutineTestKt {

  
   public static final void callFromOutside() {
      // (1)分析入口,从最外部的调用开始
      BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getIO()), (CoroutineContext)null, (CoroutineStart)null, new Function2((Continuation)null) {

        // (2)函数代码块里的任务,被封装在了继承自Continuation的一个匿名内部类对象中
        // launch开始后,进入就会调用其invoke方法,并首次执行invokeSuspend方法,这时候label为0
         int label;
        // (18) 17步返回后,标志着 stephenTest 方法中 $continuation实例的invokeSuspend方法调用完毕
        // 将调用completion的invokeSuspend方法
         // (19)这时候外部的这个label值也已经为1了,就是继续往下执行了
         public final Object invokeSuspend(Object $result) {
            Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            Object var10000;
            // (3)通过label来判断当前是到了哪一个状态
            switch (this.label) {
               case 0:
                  // (4)首先检查异常
                  ResultKt.throwOnFailure($result);
                  // (5)创建一个MySimpleTest对象,并调用其stephenTest方法
                  var10000 = new MySimpleTest();
                  // (6)将这个匿名内部类自己传进去,作为参数
                  Continuation var10001 = (Continuation)this;
                  // 将label状态设置为1,等下次再次调用invokeSuspend就会走switch的1的分支
                  this.label = 1;
                  var10000 = (MySimpleTest)var10000.stephenTest(var10001);
                   // (7) 如果 stephenTest 这个方法的返回值是COROUTINE_SUSPENDED,则表示该函数已暂停,我们也返回COROUTINE_SUSPENDED给调用者
                  // 通知这个函数是挂起函数,暂时不往下执行了
                  if (var10000 == var3) {
                     return var3;
                  }
                  // 转到MySimpleTest这个类分析 ->(8)
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  var10000 = (MySimpleTest)$result;
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            // (20)挂起和恢复流程执行完毕,打印结果
            String result = (String)var10000;
            System.out.println(result);
            return Unit.INSTANCE;
         }

         public final Continuation create(Object value, Continuation $completion) {
            return (Continuation)(new <anonymous constructor>($completion));
         }

         public final Object invoke(CoroutineScope p1, Continuation p2) {
            return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
         }

         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object p1, Object p2) {
            return this.invoke((CoroutineScope)p1, (Continuation)p2);
         }
      }, 3, (Object)null);
   }
}

以上就是我对协程挂起恢复的分析。