kotlin - kotlin 中的挂起函数/方法是如何工作的?

标签 kotlin kotlin-coroutines

这个问题的目的是了解挂起方法在底层是如何工作的,以及 IntrinsicsKt.getCOROUTINE_SUSPENDED(); 的意义是什么?

在下面的字节码中你会看到一段代码

if (var10000.resultingBlockingOp-IoAF18A((Continuation)$continuation) == var4) {
               return var4;
            }

其中 var4 是

Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();

如果函数返回,那么协程将如何执行?

在它调用自身的 invokeSuspend 方法中,这是否意味着挂起方法通过底层递归来工作?


class CoroutineExplanatoryUseCase {
    private val coroutineExplanatoryRepo: CoroutineExplanatoryRepo = CoroutineExplanatoryRepo()

    suspend fun simpleOp() {
        coroutineExplanatoryRepo.simpleBlockingOp()
    }

    suspend fun resultingBlockingOp() {
        coroutineExplanatoryRepo.resultingBlockingOp()
    }

}

生成的字节码

public final class CoroutineExplanatoryUseCase {
   private final CoroutineExplanatoryRepo coroutineExplanatoryRepo = new CoroutineExplanatoryRepo();

   @Nullable
   public final Object simpleOp(@NotNull Continuation $completion) {
      Object var10000 = this.coroutineExplanatoryRepo.simpleBlockingOp($completion);
      return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
   }

   @Nullable
   public final Object resultingBlockingOp(@NotNull Continuation var1) {
      Object $continuation;
      label20: {
         if (var1 instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var1;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl(var1) {
            // $FF: synthetic field
            Object result;
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return CoroutineExplanatoryUseCase.this.resultingBlockingOp(this);
            }
         };
      }

      Object $result = ((<undefinedtype>)$continuation).result;
      Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch (((<undefinedtype>)$continuation).label) {
         case 0:
            ResultKt.throwOnFailure($result);
            CoroutineExplanatoryRepo var10000 = this.coroutineExplanatoryRepo;
            ((<undefinedtype>)$continuation).label = 1;
            if (var10000.resultingBlockingOp-IoAF18A((Continuation)$continuation) == var4) {
               return var4;
            }
            break;
         case 1:
            ResultKt.throwOnFailure($result);
            ((Result)$result).unbox-impl();
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      return Unit.INSTANCE;
   }
}

注意:我已经知道当延续 C 恢复时,它将恢复延续 B,然后它将恢复 A,我想要理解的一点是在值与 IntrinsicsKt.getCOROUTINE_SUSPENDED() 匹配之后有一些 return 语句,如果该函数返回,那么它将最终如何执行 switch/if 中的其余分支。

所以如果你能解释一下逐步的控制流程,那就太好了!!!

最佳答案

详细回答暂停如何工作的问题并不容易,因为过程相当复杂。正如评论中所述,您可以阅读 a blog post by Marcin Moskała或者 my another answer .

但我们可以关注您的主要问题,即 IntrinsicsKt.getCOROUTINE_SUSPENDED() 的含义是什么,为什么协程从函数返回以及返回后它们如何继续。

问题

实现协程的最大挑战之一是使暂停成为可能。 挂起是指在函数内等待而不占用任何线程。我们需要意识到,这在 JVM、JavaScript 和大多数其他运行时中在技术上是不可能的。这些运行时根本不支持此类功能,我们无法像这样添加此类支持。

解决方案

Kotlin 作者介绍了一个解决方案,它几乎是 JVM/JS 之上的黑客/解决方法。这个想法是:如果我们无法在函数内部释放线程,那么......让我们返回。稍后我们可以再次调用该函数并直接跳转到我们所在的位置。

这本质上就是 Kotlin 中暂停的工作原理。这个复杂字节码的全部要点是允许在每个挂起点跳出(返回)函数并跳回每个挂起点。挂起点是代码中我们可能需要挂起的位置 - 实际上,它们是对其他挂起函数的调用。此外,我们使用延续来存储本地状态:代码偏移量和本地变量。

流程简单如下:

  1. 我们调用一个挂起函数a,它调用b,后者又调用c。我们的调用堆栈是:a -> b -> c
  2. c 挂起,因此它将其状态存储在其延续中,并以 IntrinsicsKt.getCOROUTINE_SUSPENDED() 返回。
  3. 为了释放线程,我们不仅要从 c 返回,还要从整个调用堆栈返回。 b 验证 c 返回 IntrinsicsKt.getCOROUTINE_SUSPENDED() 并且它也返回相同的值。 a 执行相同的操作,因此现在线程可以自由地执行其他操作。
  4. 当我们需要恢复时,我们通过传递其延续来调用 c(无需重建调用堆栈)。
  5. c 读取其延续,跳转到正确的偏移量,加载其局部变量并照常继续。
  6. c返回后,我们对b执行相同的操作,然后对a执行相同的操作。每个延续都保留调用者的延续,因此我们始终拥有所有延续的链。它的作用与调用堆栈类似。

所以这个复杂的字节码实际上是被切成碎片的函数代码。每个悬挂点都是一个切口。我们需要一种方法来从每个部分跳出和跳入。

最终想法

我建议阅读反编译代码时要小心。反编译器尝试猜测产生特定字节码的原始源代码是什么。但问题是:挂起函数的字节码不是编译常规 Java/Kotlin 代码的结果。甚至不可能编写与分析的字节码等效的源代码。因此,反编译器可能会感到困惑并生成并不真正正确的代码。如果您对该语言的如此深入的细节感兴趣,那么我认为您应该尝试直接读取字节码。

关于kotlin - kotlin 中的挂起函数/方法是如何工作的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76936370/

相关文章:

kotlin - Kotlin Flow:测试挂起

kotlin - 为什么在Kotlin异步方法中SecurityContextHolder.getContext()。authentication等于null?

kotlin - 暂停协程直到条件为真

Kotlin 协同例程按顺序执行,但仅在生产机器上执行

android - 如何从 Kotlin 中的 MultiSelectSpinner 获取逗号分隔的项目 ID

kotlin - 为什么不推荐使用 Kotlin channel 上的流操作?

android - 有没有办法获取设备通知历史记录?

Android Kotlin Gson 慢速Json反序列化

android - 改变 recyclerview 项目的可见性会改变其他项目的可见性

multithreading - 是否有任何领域应该优先考虑线程而不是协程?