协程代码生成-2.Suspend Functions(译)

Suspend Functions

正如在 continuation-passing style一节中所解释的,每个挂起函数的签名都会改变:编译器会添加一个continuation参数,并将返回类型更改为Any?

当我们尝试使其可挂起时,即构建状态机并生成continuation时,问题就变得棘手起来了。与挂起 lambda 不同,我们不能重用现有的类来作为continuation,因为挂起函数可以是静态的,或者一个类中可能有多个函数。

解决这个问题的一种方法是将挂起函数转换为挂起 lambda。我们可以使用挂起函数的代码生成一个挂起 lambda,并在函数内部调用该 lambda。例如,当我们有一个函数时:

kotlin 复制代码
suspend fun test() {
    suspendMe()
    suspendMe()
}

we could generate code like

kotlin 复制代码
val test$1: suspend () -> Unit = {
    suspendMe()
    suspendMe()
}

suspend fun test() {
    test$1()
}

正如可以看到的,这两段代码在语义上是相同的。顺便说一句,这就是 JS 和 Native 后端生成挂起函数的方式。此外,在 JVM 中,我们以前也是这样做的,但现在不再这样做了。

我们不再这样做的原因是堆栈跟踪。如果我们将挂起函数的主体复制到 lambda 中,堆栈跟踪将如下所示:

text 复制代码
suspendFun1$1.invokeSuspend
suspendFun2$1.invokeSuspend
suspendFun3$1.invokeSuspend
suspendFun4$1.invokeSuspend
suspendFun5$1.invokeSuspend

but we want it to look like

text 复制代码
suspendFun1
suspendFun2
suspendFun3
suspendFun4
suspendFun5

因此,我们不将函数主体移动到 lambda 中,而是保留在函数中并在其中构建状态机。但是,我们也保留了"lambda",因此我们将所有溢出的变量和标签存储在其中。这个"lambda"被称为 continuation,它实质上是协程的状态。与挂起 lambda 不同,我们将状态(称为 continuation)和挂起函数的状态机进行了拆分。

Start

尽管如此,还有另一个问题。为了正确支持completion链,我们需要创建 continuation 并将 continuation 参数存储在 completion 字段中。我们还需要支持恢复协程,也就是说,我们需要从 continuation 中获取 label 和溢出变量。因此,我们需要区分这两种情况:重新开始和继续之前暂停的执行。最简单的方法是检查 continuation 参数的类型。因此,函数的开头将如下所示:

kotlin 复制代码
fun test($completion: Continuation<Unit>): Any? {
    val $continuation =
        if ($completion is test$1) $completion
        else test$1($completion)
    // state machine
}

只要我们为每个挂起函数生成不同的 continuation 类型,检查的技巧就能让我们区分这两种情况。

然而,我们有第三种情况:递归。当我们递归调用函数时,continuation 参数的类型与我们刚刚恢复时相同(请参阅下一节)。因此,函数有三种可能的调用情况:

  1. 从另一个挂起函数或挂起 lambda 直接调用
  2. 恢复
  3. 递归

因此,我们需要至少存储另一个位的信息。我们使用 label 字段的符号位来实现。因此,函数的前缀看起来像是:

kotlin 复制代码
fun test($completion: Continuation<Unit>): Any? {
    val $continuation =
        if ($completion is test$1 && $completion.label < 0) {
            $completion.label.sign_bit = 0
            $completion
        } else test$1($completion)
    // state machine
}

在这里,我们假设在递归调用中符号位未设置,而在resume过程中,continuation 类设置了它。让我们看看如何恢复和设置这个位。

Resume

我们已经处理了启动挂起函数并创建协程(在广义上),现在可以处理恢复过程了。正如之前解释的那样,当协程(在狭义上)暂停时,它返回 COROUTINE_SUSPENDED。因此,在协程的三个关键过程中:创建、暂停和恢复,只剩下了后者。

BaseContinuationImpl.resumeWith 中,我们调用 invokeSuspend。因此,在 invokeSuspend 内部,我们调用该函数并将 this 作为continuation参数传递:

kotlin 复制代码
fun invokeSuspend(result: Result<Any?>): Any? {
    test(this)
}

然而,我们也需要设置标签的符号位:

kotlin 复制代码
fun invokeSuspend(result: Result<Any?>): Any? {
    this.label.sign_bit = 1
    test(this)
}

让我们更改函数以调用返回值的另一个函数:

kotlin 复制代码
val c: Continuation<Int>? = null

fun suspendInt(): Int = suspendCoroutine {
    c = it
}

suspend fun test() {
    val i = suspendInt()
    println(i)
}

fun main() {
    builder {
        test()
    }
    c?.resume(42)
}

当我们运行这个示例时,会打印出 42。这意味着结果以某种方式传递给了函数。我们唯一可以传递结果的地方就是invokeSuspend。此外,函数只有一个 continuation 参数。因此,我们需要将结果放入 continuation 对象本身:

kotlin 复制代码
fun invokeSuspend(result: Result<Any?>): Any? {
    this.result = result
    this.label.sign_bit = 1
    test(this)
}

然后,在函数中从 continuation 对象中获取结果:

kotlin 复制代码
fun test($completion: Continuation<Unit>): Any? {
    val $continuation =
        if ($completion is test$1 && $completion.label < 0) {
            $completion.label.sign_bit = 0
            $completion
        } else test$1($completion)
    val $result = $continuation.result
    // state machine
}

变量溢出的方式与 lambda 或函数无关。然而,我们将变量溢出到 continuation 对象中。

JVM: Parameters

让我们现在来看看我们如何处理挂起函数参数。我们不为它们生成字段,因为 lambda 只是用它们将参数从 invoke 传递到 invokeSuspend。但是,对于挂起函数,我们不需要它们:参数是本地的;因此,我们重用了本地变量的溢出。

然而,保留函数签名中的参数会破坏恢复操作。在 continuation 的 invokeSuspend 中没有足够的信息来传递它们,就像它们之前或现在一样。因此,我们只需为引用类型放置 null,并为原始类型放置零。这意味着我们不能在函数开头生成空值检查,因此它们必须在第一个状态的开头生成,我们不能在那里恢复。

例如,如果我们将 test 函数更改为接受一个参数:

kotlin 复制代码
suspend fun dummy() {}

suspend fun test(a: Int) {
    dummy()
    dummy()
}

它的 continuation 的 invokeSuspend 变成了如下形式:

kotlin 复制代码
fun invokeSuspend(result: Result<Any?>): Any? {
    this.result = result
    this.label.sign_bit = 1
    test(0, this)
}

JVM: Layout

我们现在可以推断出挂起函数的continuation布局。

理想的挂起 lambda 布局如下:

  1. 超类:kotlin/coroutines/jvm/internal/ContinuationImpl
  2. 包局部的 int 类型的 label 字段。考虑到函数使用了它,并且函数位于类外部,所以是包局部的。
  3. 包局部的字段用于存储溢出变量。同样是包局部的。
  4. 公共最终方法 invokeSuspend,类型为 (Ljava/lang/Object;)Ljava/lang/Object;。它重写了 BaseContinuationImplinvokeSuspend 方法,用于调用函数。
  5. 公共或包私有的构造函数 <init>,类型为 (Lkotlin/coroutines/Continuation;)V,它调用了 BaseContinuatonImpl

Local Suspend Functions

局部函数有些奇怪:编译器在后端生成它们的方式各不相同。局部挂起函数甚至更奇怪。

Old JVM: Unerased Suspend Lambdas

老的 JVM 后端将局部函数生成为 Lambda 表达式。因此,挂起的局部函数被生成为挂起 Lambda,例如:

kotlin 复制代码
fun main() {
    suspend fun local(i: Int) {}
}

被生成为类似于:

kotlin 复制代码
fun main() {
    val local: suspend (Int) -> Unit = {}
}

这样做可以重用捕获变量的逻辑并简化代码生成的逻辑。然而,由于旧后端的限制,其 createinvoke 是未擦除的。换句话说,编译器复制它们,生成未擦除和擦除的副本。未擦除的副本接受带类型的参数,并包含 Lambda 的 invokecreate 的逻辑。另一个副本只接受 Any?。 局部挂起函数的布局如下:

  1. 超类型:kotlin/coroutines/jvm/internal/SuspendLambdakotlin/jvm/functions/Function{N}
  2. 包私有捕获变量
  3. 私有的整型 label 字段。私有的,因为它仅在 Lambda 本身中使用。
  4. 私有参数字段。可见性的原因与 label 字段相同。
  5. 用于溢出变量的私有字段。同样是私有的。
  6. 公共最终方法 invokeSuspend,类型为 (Ljava/lang/Object;)Ljava/lang/Object;。它重写了 BaseContinuationImplinvokeSuspend
  7. 公共最终 create 方法,类型为 (<params>,Lkotlin/coroutines/Continuation)Lkotlin/coroutines/Continuation<params> 的类型被擦除。
  8. 公共最终 create 方法,类型为 (<params>,Lkotlin/coroutines/Continuation)Lkotlin/coroutines/Continuation<params> 的类型未被擦除。
  9. 公共最终 invoke 方法,类型为 (<params>,Ljava/lang/Object;)Ljava/lang/Object;<params> 被擦除。
  10. 公共最终 invoke 方法,类型为 (<params>,Lkotlin/coroutines/Continuation;)Ljava/lang/Object;<params> 未被擦除。
  11. 公共或包私有构造函数:<init>,类型为 (<captured-variables>,Lkotlin/coroutines/Continuation;)V,在其中我们使用参数调用 SuspendLambda 的构造函数,并初始化捕获变量。 至于挂起 Lambda,编译器知道函数的 arity,但是完成函数是作为参数传递给构造函数的。

注意:这种实现存在大量的 bug。例如,局部挂起函数几乎无法递归。

JVM_IR: Static Functions

另一方面,JVM_IR 将局部函数生成为静态函数,并将捕获的变量作为第一个参数传递。因此,挂起的局部函数也被生成为静态函数。这样可以减小代码大小和方法数量,并且启用尾调用优化。

一个示例:

kotlin 复制代码
fun main() {
    val aa: Long = 1
    suspend fun local(i: Int) {
        println(aa)
    }
}

is generated as something like

kotlin 复制代码
suspend fun main$1(aa: Long, i: Int) {
    println(aa)
}

fun main() {
    val aa: Long = 1
}

Tail-Call Optimization

有人可能已经注意到,我们并不总是需要状态机。例如,当挂起函数根本不调用另一个挂起函数时。由于每个挂起调用都会创建一个 continuation,将其放在循环中成本相当高。出于这两个原因,我们不为将所有挂起调用置于尾位置的挂起函数生成 continuation 类和状态机。因为它们没有办法在函数中间挂起,它们不需要其中任何一个:它们只有一个状态。

尾调用函数示例:

kotlin 复制代码
suspend fun returnsInt() = suspendCoroutine<Int> { it.resume(42) }

suspend fun tailCall1(): Int {
    return returnsInt()
}

suspend fun tailCall2() = returnsInt()

对于这两个函数,编译器生成以下字节码(在 CoroutineTransformerMethodVisitor 之前):

text 复制代码
INVOKESTATIC InlineMarker.beforeInlineCall
ALOAD 1 // continuation
ICONST 0 // before suspend marker
INVOKESTATIC InlineMarker.mark
INVOKESTATIC returnsInt()
ICONST 1 // after suspend marker
INVOKESTATIC InlineMarker.mark
INVOKESTATIC InlineMarker.afterInlineCall
ARETURN

经过尾调用优化后,代码变为:

text 复制代码
ALOAD 1 // continuation
INVOKESTATIC returnsInt
ARETURN

检查函数是否是尾调用很简单:检查所有(可达的)挂起点是否:

  1. 不在 try-catch 块内
  2. 紧跟着的是带有可选分支或堆栈修改的 ARETURN,有一个显著的例外:GETSTATIC Unit; ARETURN(稍后会详细说明)。

MethodNodeExaminer 包含了此检查的逻辑。由于我们在两个后端中都使用相同的状态机构建器(因为我们应该支持 JVM_IR 中的字节码内联),因此逻辑是共享的。

请注意,由于我们不创建状态机,所以无需溢出变量,因此我们也不会创建 continuation 类。因此,completion链将缺少一个链接:

kotlin 复制代码
suspend fun returnsInt1() = suspendCoroutine<Int> { it.resume(42) }

suspend fun returnsInt2(): Int {
    val result = returnsInt1()
    println(result)
    return result
}

suspend fun returnsInt3() = returnsInt2()

suspend fun main() {
    println(returnsInt3())
}

如果没有尾调用优化,完成链会如下所示:

text 复制代码
          null<------+
                     |
      +-----------+  |
   +->+returnsInt1|  |
   |  +-----------+  |
   |  |completion +--+
   |  +-----------+
   |
   |
   |  +-----------+
   |  |returnsInt2+<-+
   |  +-----------+  |
   +--+completion |  |
      +-----------+  |
                     |
      +-----------+  |
   +->+returnsInt3|  |
   |  +-----------+  |
   |  |completion +--+
   |  +-----------+
   |
   |
   |  +-----------+
   |  |    main   |
   |  +-----------+
   +--+completion |
      +-----------+

但是使用尾调用优化后,它变成

text 复制代码
   +----->null
   |
   |  +-----------+
   |  |returnsInt2+<-+
   |  +-----------+  |
   +--+completion |  |
      +-----------+  |
                     |
      +-----------+  |
      |    main   |  |
      +-----------+  |
      |completion +--+
      +-----------+

returnInt1returnInt3 是尾调用,没有continuation。

在旧的 JVM 后端中,本地挂起函数是 lambda,它们不支持尾调用优化,但是由 JVM_IR 生成的本地挂起函数支持。

Redundant Locals Elimination

正如在有关变量溢出的部分所解释的那样,内联器在内联之前溢出堆栈,并在内联之后取消溢出。这导致了一堆重复的 ASTORE 和 ALOAD 指令,这可能会破坏尾调用消除,因为在挂起点和 ARETURN 之间可能会有一系列 ASTORE; ALOAD。这种字节码修改简化了链条,并使这些情况下的尾调用优化成为可能。

Tail-Call Optimization for Functions Returning Unit

如果我们想让返回 Unit 的挂起函数支持尾调用,那么就会面临一些挑战。让我们看看其中一个挑战。如果函数返回 Unit,则 return 关键字是可选的:

kotlin 复制代码
suspend fun returnsUnit() = suspendCoroutine<Unit> { it.resume(Unit) }

suspend fun tailCall1() {
    return returnsUnit()
}

suspend fun tailCall2() = returnsUnit()

suspend fun tailCall3() {
    returnsUnit()
}

在这个例子中,tailCall1tailCall2 被普通的尾调用优化所覆盖。然而,最后一个函数则不同。代码生成器生成了以下字节码:

text 复制代码
INVOKESTATIC InlineMarker.beforeInlineCall
ALOAD 1 // continuation
ICONST 0 // before suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC returnsUnit()
ICONST 1 // after suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC InlineMarker.afterInlineCall()V
POP
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

正如你所看到的,UnitPOP 出栈,然后被重新推送到栈上并返回。不幸的是,我们不能简单地移除 POP; GETSTATIC kotlin/Unit.INSTANCE:如果我们用 returnsUnit 替换 returnsInt,字节码是相同的。由于在 CoroutineTransformerMethodVisitor 中我们无法获取挂起调用的返回类型信息,我们将所有这些调用视为只是 Any?,我们需要使用标记来标记返回 Unit 的函数调用。该标记与挂起标记类似,但参数不同:ICONST_2。因此,tailCall3 函数的完整字节码如下:

text 复制代码
INVOKESTATIC InlineMarker.beforeInlineCall
ALOAD 1 // continuation
ICONST 0 // before suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC returnsUnit()
ICONST_2 // returns unit marker
INVOKESTATIC InlineMarker.mark(I)V
ICONST 1 // after suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC InlineMarker.afterInlineCall
POP
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

尾调用优化后,如预期的那样,没有状态机:

text 复制代码
ALOAD 1 // continuation
INVOKESTATIC returnsUnit()
ARETURN

我们暂时将returnsUnit替换为returnsInt

kotlin 复制代码
suspend fun returnsInt() = suspendCoroutine<Int> { it.resume(42) }

suspend fun tailCall() {
    returnsInt()
}

如前所述,不能简单地删除POP; GETSTATIC kotlin/Unit.INSTANCE,因为在这种情况下,返回Unit的函数将返回Int。但是,状态机中只能有一个状态。因此,我们只需保留POP; GETSTATIC kotlin/Unit.INSTANCE

text 复制代码
ALOAD 1 // continuation
INVOKESTATIC returnsInt()
POP
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

然而,这里有一个问题。由于completion链缺少一个链接,有时会出现返回非Unit值的返回Unit的挂起函数的情况:

kotlin 复制代码
import kotlin.coroutines.*

var c: Continuation<Int>? = null

suspend fun returnsInt(): Int = suspendCoroutine { c = it }

suspend fun returnUnit() {
    returnsInt()
}

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(Continuation(EmptyCoroutineContext) {
        it.getOrThrow()
    })
}

fun main() {
    builder {
        println(returnUnit())
    }

    c?.resume(42)
}

这个例子,就像前面的一个一样,一个返回Unit的尾调用函数(returnUnit)调用了一个返回非Unit的函数(returnsInt)。编译器生成了以下的completion链:

text 复制代码
           null<-----+
                     |
      +-----------+  |
   +->+ builder$1 |  |
   |  +-----------+  |
   |  |completion +--+
   |  +-----------+
   |
   |
   |  +-----------+
   |  |   main$1  |
   |  +-----------+
   +--+completion |
      +-----------+

这是正确的;编译器只生成了一个 continuation:main$1。此外,它被传递给了returnsUnit,然后传递给了returnsInt,最后存储在 c 变量中。

现在,让我们看看在构建状态机之前,codegen在 main$1 lambda中生成了什么:

text 复制代码
INVOKESTATIC InlineMarker.beforeInlineCall
ALOAD 1 // continuation
ICONST 0 // before suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC returnsUnit()Ljava/lang/Object;
ICONST_2 // returns unit marker
INVOKESTATIC InlineMarker.mark(I)V
ICONST 1 // after suspending marker
INVOKESTATIC InlineMarker.mark(I)V
INVOKESTATIC InlineMarker.afterInlineCall
INVOKEVIRTUAL println(Ljava/lang/Object;)V
GETSTATIC kotlin/Unit.INSTANCE
ARETURN

我将内联的 println 替换为了一个调用,以便更清晰地理解。在转换为状态机之后,代码如下所示:

kotlin 复制代码
fun invokeSuspend($result: Any?): Any? {
    when (this.label) {
        0 -> {
            this.label = 1
            $result = returnsUnit(this)
            if ($result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
            goto 1
        }
        1 -> {
            println($result)
            return Unit
        }
        else -> {
            throw IllegalStateException("call to 'resume' before 'invoke' with coroutine")
        }
    }
}

在恢复协程后(其 label 值为 1),$result42,并将被打印出来。没错,一个返回 Unit 的函数似乎返回了非 Unit 的值。为了解决这个问题,我们将返回单位标记替换为 POP; GETSTATIC kotlin/Unit.INSTANCE 序列。这样,我们就忽略了传递给 resume 的值,就像没有挂起一样。顺便说一句,在 callSuspendcallSuspendBy 函数中,我们也做同样的事情。

然而,我们并不总是能够进行替换,就像下面的例子所示:

kotlin 复制代码
import kotlin.coroutines.*

var c: Continuation<*>? = null

suspend fun <T> tx(lambda: () -> T): T = suspendCoroutine { c = it; lambda() }

object Dummy

interface Base<T> {
    suspend fun generic(): T
}

class Derived: Base<Unit> {
    override suspend fun generic() {
        tx { Dummy }
    }
}

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(object: Continuation<Unit> {
        override val context = EmptyCoroutineContext
        override fun resumeWith(result: Result<Unit>){
            result.getOrThrow()
        }
    })
}

fun main() {
    var res: Any? = null

    builder {
        val base: Base<*> = Derived()
        res = base.generic()
    }

    (c as? Continuation<Dummy>)?.resume(Dummy)

    println(res)
}

在这个例子中,我们无法确定 generic 是否返回 Unit。在这种情况下,编译器禁用尾调用优化。更一般地说,如果函数重写了返回非 Unit 类型的函数,那么编译器会禁用返回 Unit 的函数的尾调用优化。

Returning Inline Classes

在 1.4 之前,如果一个挂起函数返回一个内联类,那么该类的值会被装箱。对于包含引用类型的内联类来说,这是不可取的,因为它会导致额外的分配。因此,如果编译器可以验证调用者返回一个内联类,那么它就不会在调用者中生成装箱指令,并且在调用方中也不会生成拆箱指令。否则,调用者将返回一个装箱的值,就像下面的示例中一样:

kotlin 复制代码
inline class IC(val a: Any)

interface I {
    suspend fun overrideMe(): Any
}

class C : I {
    override suspend fun overrideMe(): IC = IC("OK")
}

suspend fun main() {
    val i = C()
    println(i.overrideMe())
}

在这里,编译器无法验证调用点总是期望内联类。因此,overrideMe 总是对类进行装箱。

然而,这种优化并不像看起来那么简单。挂起调用的执行有两条路径:直接路径,当调用方返回给调用者时;和恢复路径,当调用方返回给 invokeSuspend,然后返回给 resumeWith,后者调用 completion.resumeWith,它调用 invokeSuspend,它调用调用方。在直接路径中(最常见的情况),类被解包。

然而,在恢复路径中,我们应该对内联类进行装箱(在这种情况下,我们不太关心性能)。 BaseContinuationImpl.resumeWith 调用 invokeSuspend,它期望 invokeSuspend 的返回类型是 "T | COROUTINE_SUSPENDED",其中 T 是此处的装箱内联类。如果违反了这个契约,就会导致以下示例中抛出异常:

kotlin 复制代码
import kotlin.coroutines.*

fun main() {
    builder {
        signInFlowStepFirst()
    }
    continuation!!.resumeWithException(Exception("BOOYA"))
}

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(object : Continuation<Unit> {
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<Unit>) {
            result.getOrThrow()
        }
    })
}

var continuation: Continuation<Unit>? = null

suspend fun suspendMe() = suspendCoroutine<Unit> { continuation = it }

@Suppress("RESULT_CLASS_IN_RETURN_TYPE")
suspend fun signInFlowStepFirst(): Result<Unit> = try {
    Result.success(suspendMe())
} catch (e: Exception) {
    Result.failure(e)
}

导致错误的解释并不简单:

  1. signInFlowStepFirst 调用 suspendMe 并暂停。
  2. 我们使用异常恢复了执行。
  3. signInFlowStepFirst 中,我们使用 Result 类包装了异常,就像包裹在一块卷饼中一样。
  4. 由于这是恢复路径(我们恢复了执行),执行返回到 invokeSuspend,它将 Result$Failure 返回给 BaseContinuationImpl.resumeWith
  5. BaseContinuationImpl.resumeWith 使用另一个 Result 包装了 Result$Failure,但由于 Result 是内联类,操作的结果(双关语不是故意的)是相同的 Result$Failure
  6. BaseContinuationImpl.resumeWith 调用 completion.resumeWith,将 Result$Failure 作为参数传递,这被视为 completionresumeWithException

所以,如果函数返回内联类且编译器已经优化了装箱,我们需要在 invokeSuspend 中对内联类进行装箱,以及在可调用引用中。这样修复了 invokeSuspend 的协程约定。

然而,在直接路径中,生成的代码期望是一个非装箱的值。因此,在调用者的恢复路径中,我们应该对其进行非装箱处理。我们可以在 invokeSuspend 和状态机中的取消溢出处对其进行非装箱处理。考虑以下代码片段:

kotlin 复制代码
import kotlin.coroutines.*

inline class IC(val a: Any)

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(Continuation(EmptyCoroutineContext) { it.getOrThrow() })
}

var c: Continuation<Any>? = null

suspend fun returnsIC() = suspendCoroutine<IC> { c = it as Continuation<Any> }
suspend fun returnsAny() = suspendCoroutine<Any> { c = it }

suspend fun test() {
    println(returnsIC())
    println(returnsAny())
}

fun main() {
    builder {
        test()
    }
    c?.resume(IC("OK1"))
    c?.resume("OK2")
}

这里,我们两次恢复 test 函数,一次使用内联类,另一次使用普通类。然而,我们只需要在第一次恢复时对值进行装箱:在第一次恢复期间。这意味着如果我们想要进行装箱,我们需要在 invokeSuspend 中添加复杂的逻辑。在状态机内部进行装箱会更简单。

Unbox Inline Class Markers

这个状态机部分解释了编译器如何将顺序代码转换为状态机。简单来说,它在挂起点周围生成标记。因此,如果我们想要从代码生成器传递信息到状态机构建器或内联器,一些标记是必不可少的。自然地,我们为在恢复路径上需要生成的拆箱序列生成标记。

考虑以下示例。

kotlin 复制代码
inline class IC(val s: String)

suspend fun ic() = IC("OK")

suspend fun main() {
    println(ic().s)
}

代码生成器必须为 main 函数中的 IC 类生成一个拆箱序列,因此类构建器将其移动到恢复路径,因为 main 不是尾调用函数,因此具有状态机,而不像 ic 那样。此外,它应该告诉构建器这些指令要移动。因此,它将它们用新的标记包围起来,现在的标记id是 89

text 复制代码
INVOKESTATIC ic(Lkolint/coroutines/Continuation;)Ljava/lang/Object;
BIPUSH 8
INVOKESTATIC kotlin/jvm/internal/InlineMarker(I)V
CHECKCAST LIC;
INVOKEVIRTUAL IC.unbox-impl()Ljava/lang/String;
CHECKCAST Ljava/lang/Object;
BIPUSH 9
INVOKESTATIC kotlin/jvm/internal/InlineMarker(I)V

请注意 CHECKCAST Ljava/lang/Object; 是结束标记的一部分。我们需要生成它,以免干扰字节码分析。否则,字节码分析会认为挂起调用的返回类型是 String,而不是 Any?。将拆箱移动到恢复路径后,构建器会删除此转换。

通过这种方式传递信息也适用于内联。考虑以下示例:

kotlin 复制代码
import kotlin.coroutines.*

// Library lib1
inline class IC(val a: Any)

var c: Continuation<Any>? = null

suspend fun returnsIC() = suspendCoroutine<IC> { c = it as Continuation<Any> }

// Library lib2 depends on lib2

suspend inline fun inlineMe() {
    println(returnsIC())
}

// Main module

fun builder(c: suspend () -> Unit) {
    c.startCoroutine(Continuation(EmptyCoroutineContext) { it.getOrThrow() })
}

suspend fun test() {
    inlineMe()
}

fun main() {
    builder {
        test()
    }
    c?.resume(IC("OK1"))
}

这里,inlineMe$$forInline 没有状态机,因此,直接路径类似于恢复路径。然而,代码生成器生成了标记,内联器内联了这些标记:

text 复制代码
ICONST_1
INVOKESTATIC kotlin.jvm.internal.InlineMarker.mark(I)V
// The suspension point
ICONST_2
INVOKESTATIC kotlin.jvm.internal.InlineMarker.mark(I)V
ICONST_8
INVOKESTATIC kotlin.jvm.internal.InlineMarker.mark(I)V
// After this marker, there should be a call to box-impl
INVOKEVIRTUAL IC.unbox-impl(Ljava/lang/Object;)LIC;
CHECKCAST Ljava/lang/Object;
BIPUSH 9
INVOKESTATIC kotlin/jvm/internal/InlineMarker(I)V

最后,在内联后,状态机生成器将取消装箱移到了恢复路径中。

相关推荐
人间有清欢18 小时前
十、kotlin的协程
kotlin
吾爱星辰18 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer18 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
一丝晨光1 天前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
500了2 天前
Kotlin基本知识
android·开发语言·kotlin
陈亦康2 天前
Armeria gPRC 高级特性 - 装饰器、无框架请求、阻塞处理器、Nacos集成、负载均衡、rpc异常处理、文档服务......
kotlin·grpc·armeria
奋斗的小鹰3 天前
kotlin 委托
android·开发语言·kotlin
Wency(王斯-CUEB)3 天前
【文献阅读】政府数字治理的改善是否促进了自然资源管理?基于智慧城市试点的准自然实验
android·kotlin·智慧城市
中游鱼3 天前
Visual Studio C# 编写加密火星坐标转换
kotlin·c#·visual studio
500了3 天前
kotlin协程
开发语言·python·kotlin