kotlin协程学习——Continuation

协程的内部实现

有一类人不能接受只是开车,他们需要打开引擎盖来了解汽车的运作方式。我就是其中之一,所以我必须找出协程是如何工作的。如果你也是这样的人,你会喜欢这一章。如果不是,你可以跳过它。

这一章不会介绍任何您可能使用的新工具,它纯粹是解释性的。它试图以令人满意的程度解释协程的工作原理。关键教训是:

  • 挂起函数就像状态机一样, 在函数开始和每个挂起函数调用后都对应一个状态值.
  • 状态的编号和本地数据都保存在 Continuation 对象中。
  • 一个函数的 Continuation 装饰着其调用者函数的 Continuation;因此,所有这些 Continuation 表示一个调用栈,在恢复或恢复的函数完成时使用。

如果你还有兴趣继续, 那我们一起

Continuation是可传递的

有几种方式可以实现暂停函数,但Kotlin团队选择了一种称为"continuation-passing style"的选项。这意味着continuations(在上一章中解释)作为参数从一个函数传递到另一个函数。按照惯例,continuation位于参数列表的最后一个位置。

你可能已经注意到底层的结果类型与最初声明的类型不同。它已经变成了 Any 或 Any?。为什么?原因是,一个挂起函数可能会被挂起,因此它可能不会返回一个声明的类型。在这种情况下,它返回一个特殊的 COROUTINE_SUSPENDED 标记,我们稍后会在实践中看到。现在只需要注意,由于 getUser 可能返回 User? 或 COROUTINE_SUSPENDED(它是 Any 类型),其结果类型必须是 User? 和 Any 最接近的超类型,因此是 Any?。也许有一天 Kotlin 将引入联合类型,在这种情况下我们将有 User? | COROUTINE_SUSPENDED。

A very simple function

深入研究一下,我们从一个非常简单的函数开始,它在延迟之前和之后打印一些东西。

您已经可以推断出 myFunction 函数在底层的函数签名将是什么样的:

kotlin 复制代码
fun myFunction(continuation: Continuation<*>): Any

myFunction 函数需要自己的 Continuation 来记住它的状态,接下来我们把它命名为 MyFunctionContinuation(实际的 Continuation 是一个对象表达式,没有名字,但这样命名更容易解释)。在 myFunction 函数的函数体开头,它会使用它自己的 Continuation(MyFunctionContinuation)包装传入的 Continuation 参数。

kotlin 复制代码
val continuation = MyFunctionContinuation(continuation)

这应该只在 continuation 没有被包装过的情况下进行。如果已经被包装了,那么这是恢复过程的一部分,我们应该保持 continuation 不变。(现在可能有点困惑,但稍后你会更清楚为什么。)

kotlin 复制代码
val continuation =
if (continuation is MyFunctionContinuation) continuation else MyFunctionContinuation(continuation)

或者简单转换为:

kotlin 复制代码
val continuation = continuation as? MyFunctionContinuation ?: MyFunctionContinuation(continuation)

现在可以讨论下这个具体代码逻辑了

kotlin 复制代码
suspend fun myFunction() { 
    println("Before") 
    delay(1000) // suspending 
    println("After")
}

该函数可以从两个地方开始执行:从头开始(第一次调用)或者从挂起点后的位置开始执行(在恢复 continuation 时)。为了确定当前状态,我们使用一个叫做 label 的字段。在开始时,它为 0,因此函数将从头开始执行。但是,在每个挂起点之前,我们需要将 label 设置为下一个状态,这样在恢复时就可以从挂起点后面的位置继续执行。这里的实际机制有一点更加复杂,因为标签的第一位也会被更改,并且这个更改会被暂停函数检查。这个机制是为了支持递归而需要的,但为了简单起见,先这么假设.

最后一个重要的部分已经在上面的代码片段中呈现出来了。当 delay 被挂起时,它会返回 COROUTINE_SUSPENDED,然后 myFunction 也会返回 COROUTINE_SUSPENDED;这个调用它的函数也会这样做,以及调用这个函数的函数,以及所有其他函数,一直到调用栈的顶部14。这是一个挂起如何结束所有这些函数,并将线程留给其他可运行项(包括协程)使用的方式。

在我们进一步探讨之前,让我们分析一下上面的代码。如果这个 delay 调用没有返回 COROUTINE_SUSPENDED 会发生什么?如果它只返回 Unit 而不是 COROUTINE_SUSPENDED 呢(我们知道它不会这样做,但是让我们假设)?请注意,如果 delay 只返回 Unit,我们将只是移动到下一个状态,并且该函数将像任何其他函数一样运行。

现在,让我们来谈谈 continuation,它是作为匿名类实现的。简化后,它看起来像这样:

当函数 a 调用函数 b 时,虚拟机需要在某个地方存储 a 的状态,以及 b 完成后应返回执行的地址。所有这些都存储在一个称为调用堆栈的结构中。问题在于,当我们暂停时,我们释放了一个线程,因此清除了我们的调用堆栈。因此,在恢复时,调用堆栈无用。取而代之的是,continuation 作为调用堆栈。每个 continuation 保留我们暂停时的状态(作为标签),函数的本地变量和参数(作为字段),以及引用调用此函数的函数的 continuation。一个 continuation 引用另一个 continuation,依此类推。因此,我们的 continuation 就像一个巨大的洋葱:它保留了通常保存在调用堆栈上的所有内容。请看下面的示例:

例如,想象一个这样的情况:函数a调用函数b,函数b又调用函数c,函数c被挂起。在恢复时,c的 continuation首先恢复c函数。一旦这个函数完成了,c continuation恢复b continuation调用b函数。一旦它完成了,b continuation恢复a continuation,调用a函数。

整个过程可以用以下草图表示:

在异常处理上也是类似的:未捕获的异常在 resumeWith 中被捕获,然后被包装成 Result.failure(e),然后调用我们函数的函数会使用这个结果进行恢复。

希望这些内容能让你了解到当我们进行挂起时所发生的事情。状态需要存储在 continuation 中,并需要支持挂起机制。当我们恢复时,需要从 continuation 中恢复状态,并根据需要使用结果或抛出异常。

希望这些让你了解到了我们在暂停时所做的一切。需要在 continuation 中存储状态,并支持暂停机制。当我们恢复时,需要从 continuation 中恢复状态并使用结果或抛出异常。

The actual code

Continuations和挂起函数实际编译成的代码更加复杂,因为它包括了优化和一些额外的机制,例如:

  • 构建更好的异常堆栈跟踪;
  • 添加协程挂起截获
  • 在不同的级别上进行优化(例如删除未使用的变量或尾递归优化)

挂起函数的性能

使用挂起函数而不是常规函数的成本是多少?当深入了解内部实现时,很多人可能会认为成本很高,但事实并非如此。将函数分成状态是廉价的,因为数字比较和执行跳转几乎不会产生任何开销。保存状态在continuation中也很便宜。我们不会复制本地变量:我们让新变量指向内存中的同一点。唯一需要成本的操作是创建一个continuation类,但这仍然不是大问题。如果你不担心 RxJava 或回调的性能,那么你肯定不用担心挂起函数的性能。

结语

实际上,协程底层的机制比我所描述的更加复杂,但是我希望你能对协程的内部有一些了解

  • 挂起函数类似于状态机,有一个可能的状态在函数的开始和每个挂起函数调用后。

  • 标识状态的标签和本地数据都存储在continuation对象中。

  • 一个函数的 continuation 装饰着它的调用函数的 continuation,因此所有这些 continuation 代表了一个用于恢复或者已恢复函数的调用栈。

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀

相关推荐
fanqi9875 分钟前
Android模拟器ADB异常断开一个容易忽视原因的记录
android·adb·android studio
冬奇Lab8 分钟前
稳定性性能系列之五——Native Crash深度分析:工具实战
android·性能优化·debug
峥嵘life12 分钟前
深耕Android技术——2025年CSDN博客之星总评选深度总结
android
无言Echo12 分钟前
App 深色模式切换流程简述(api32)及相关bug
android
GoldenPlayer13 分钟前
Android网络请求报错(直接请求http)
android
モンキー・D・小菜鸡儿13 分钟前
kotlin 斗牛小游戏
kotlin·小游戏
花卷HJ15 分钟前
Android 多媒体文件工具类封装(MediaFileUtils)
android·java
csj5016 分钟前
安卓基础之《(11)—数据存储(1)共享参数SharedPreferences》
android
走在路上的菜鸟16 分钟前
Android学Dart学习笔记第二十七节 异步编程
android·笔记·学习·flutter
哆啦安全17 分钟前
Android智能调试分析工具V7.5
android