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老皮!!!欢迎大家来找我探讨交流👀

相关推荐
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌3 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley5 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei7 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng7 小时前
安卓多渠道apk配置不同签名
android
枫_feng7 小时前
AOSP开发环境配置
android·安卓
叶羽西7 小时前
Android Studio打开一个外部的Android app程序
android·ide·android studio
qq_171538859 小时前
利用Spring Cloud Gateway Predicate优化微服务路由策略
android·javascript·微服务
Vincent(朱志强)10 小时前
设计模式详解(十二):单例模式——Singleton
android·单例模式·设计模式
mmsx11 小时前
android 登录界面编写
android·登录界面