Kotlin协程挂起恢复源码解读

背景

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. 续体是一个较为抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成一个又一个续体。
1. 在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);
   }
}

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

### Kotlin 协程中的挂起函数 #### 定义与基本语法 在 Kotlin 中,为了使函数能够被协程调用并支持挂起操作,在 `fun` 关键字前加上 `suspend` 修饰符即可定义一个挂起函数。这允许该函数内部执行长时间运行的任务而不阻塞线程[^2]。 ```kotlin public suspend fun testSuspendFunction(): String { delay(1000L) // 非阻塞方式等待一秒钟 return "Finished" } ``` 上述代码展示了如何创建简单的挂起函数,并利用内置的 `delay()` 函数模拟异步延迟效果而不会冻结任何实际线程资源。 #### 调用场景 当需要在一个非主线程环境中发起网络请求、读写文件或其他耗时任务时,可以考虑使用挂起函数来简化编程模型。由于这些操作通常涉及 I/O 或者计算密集型工作,因此非常适合采用基于协程的方式处理以提高应用程序响应速度和用户体验质量。 #### 常见问题解答 - **为什么不能直接从普通函数中调用挂起函数?** 这是因为常规函数不具备暂停的能力;只有标记为`suspend` 的特殊函数才能安全地调用其他挂起函数。如果尝试这样做,则编译器会报错提示无法解析符号或非法表达式。 - **怎样捕获挂起函数抛出异常的情况?** 可以通过 try-catch 结构包裹对挂起函数的调用来捕捉可能发生的错误状况: ```kotlin try { val result = withContext(Dispatchers.IO) { someSuspendingCallThatMayThrowException() } } catch(e: Exception){ println("Caught exception $e") } ``` 这里还涉及到改变调度上下文以便更好地管理不同类型的并发活动。 - **能否让多个挂起点共享相同的锁机制从而协调它们之间的顺序关系?** 确实存在这样的需求场景下,可借助于 `Mutex` 实现同步控制逻辑。例如下面的例子说明了两个不同的挂起动作按照特定次序依次完成的过程[^1]: ```kotlin val mutex = Mutex() launch { mutex.withLock { println("First action starts.") delay(500) println("First action ends.") } mutex.withLock { println("Second action begins after first one finished.") delay(500) println("Second action concludes.") } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值