继续在 Kotlin 协程中如何工作?

c-a*_*-an 9 kotlin kotlin-coroutines

我正在研究CPS。我想知道它是如何工作的。

Object createPost(
    Token token,
    Item item,
    Continuation<Post> const){...}
Run Code Online (Sandbox Code Playgroud)
interface Continuation<in T> {
    val context: CoroutineContext
    fun resume(value: T)
    fun resumeWithException(exception: Throwable)
}
Run Code Online (Sandbox Code Playgroud)

人们说 CPS 只是回调,仅此而已。

  1. 我不知道为什么这里使用接口作为参数。
  2. 我不知道<in T>Continuation 界面中做了什么。
  3. Continuation 是一个参数,但是,它实际上在内部做什么以及如何在幕后调用它?

bro*_*oot 21

最终用户视角

对于最终用户来说,情况相对简单:延续代表被挂起的执行流。resume()它允许通过调用或来恢复执行resumeWithException()

例如,假设我们要暂停一秒钟然后恢复执行。我们要求协程机制挂起,它提供一个延续对象,我们存储它,稍后我们调用resume()它。延续对象“知道”如何恢复执行:

suspend fun foo() {
    println("foo:1")
    val result = suspendCoroutine { cont ->
        thread {
            Thread.sleep(1000)
            cont.resume("OK")
        }
    }
    println("foo:2:$result")
}
Run Code Online (Sandbox Code Playgroud)

suspendCoroutine()是暂停并获取继续以稍后恢复的可能方法之一。thread()Thread.sleep()用于演示目的 - 通常我们应该使用delay()

很多时候我们会停下来获取某种数据。这就是连续性支持使用结果值恢复的原因。在上面的示例中,我们可以看到 的结果suspendCoroutine()存储为result,并且我们通过传递 来恢复延续"OK"。这样恢复result持有后"OK"。这就解释了<in T>

内部结构

这要复杂得多。Kotlin 在不支持协程或挂起的运行时中执行。例如,JVM 无法在不阻塞任何线程的情况下在函数内等待。这根本不可能(我在这里故意忽略 Project Loom)。为了实现这一点,Kotlin 编译器必须操作字节码,并且延续在这个过程中扮演着重要的角色。

正如您所注意到的,每个挂起函数都会接收额外的Continuation类型参数。该对象用于控制恢复的过程,它有助于返回到函数调用者并保存当前的协程上下文。此外,挂起函数返回Any/Object以允许向调用者发出其状态信号。

假设我们有另一个函数调用第一个函数:

suspend fun bar() {
    println("bar:1")
    foo()
    println("bar:2")
}
Run Code Online (Sandbox Code Playgroud)

然后我们调用bar(). foo()两者的字节码bar()比您通过查看上面的源代码所预期的要复杂得多。这就是正在发生的事情:

  1. bar()是通过其调用者的延续来调用的(让我们暂时忽略这意味着什么)。
  2. bar()检查它是否“拥有”传递的延续。它没有看到,所以它假设这是其调用者的延续,并且这是 的初始执行bar()
  3. bar()创建自己的延续对象并将调用者的延续存储在其中。
  4. bar()开始正常执行并到达foo()目的。
  5. 它在其延续中存储局部状态、代码偏移、局部变量的值等。
  6. bar()调用foo()传递其延续。
  7. foo()检查它是否拥有传递的延续。事实并非如此,延续由 拥有bar(),因此foo()创建自己的延续,bar()将 的延续存储在其中并开始正常执行。
  8. 执行到达suspendCoroutine()并与之前类似,本地状态存储在foo()的延续内部。
  9. 的继续部分foo()在传递给 的 lambda 内提供给最终用户suspendCoroutine()
  10. 现在,foo()想要暂停其执行,所以它...返回...是的,如前所述,在不阻塞线程的情况下等待是不可能的,因此释放线程的唯一方法是从函数返回。
  11. foo()返回一个特殊值,表示:“执行已暂停”。
  12. bar()读取这个特殊值并且也暂停,所以也立即返回。
  13. 整个调用堆栈折叠起来,线程可以自由地去做其他事情。
  14. 1 秒过去了,我们调用cont.resume().
  15. Continuationfoo()知道如何从该点继续执行suspendCoroutine()
  16. 继续调用将foo()自身作为参数传递的函数。
  17. foo()检查它是否拥有传递的延续 - 这次它拥有,因此它假设这不是对 的初始调用foo(),而是恢复执行的请求。它从延续中读取存储的状态,加载局部变量并跳转到正确的代码偏移量。
  18. 执行正常进行,直到到达需要从foo()to返回的点bar()
  19. foo()知道这次它不是由 调用的bar(),所以简单地返回是行不通的。但它仍然保留其调用者的延续,因此在需要返回的bar()地方暂停。foo()
  20. foo()返回具有神奇值的内容:“恢复我的调用者的继续”。
  21. 继续bar()从执行点恢复foo()
  22. 过程继续。

如您所见,这非常复杂。通常,协程的用户不需要了解它们的内部工作原理。

其他重要注意事项:

  • 如果foo()不暂停,它将正常返回bar()bar()继续照常执行。这是为了在不需要挂起的情况下减少整个过程的开销。
  • 恢复时,延续不会直接调用它们的函数,而是要求调度程序执行此操作。调度程序存储在内部CoroutineContext,因此也存储在延续内部。
  • 请注意,由于延续保留了对调用者延续的引用,因此它们形成了延续链。这可用于生成堆栈跟踪,因为挂起时真实的调用堆栈已丢失。