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 函数在底层的函数签名将是什么样的:

fun myFunction(continuation: Continuation<*>): Any

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

val continuation = MyFunctionContinuation(continuation)

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

val continuation =
if (continuation is MyFunctionContinuation) continuation else MyFunctionContinuation(continuation)

或者简单转换为:

val continuation = continuation as? MyFunctionContinuation ?: MyFunctionContinuation(continuation)

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

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 代表了一个用于恢复或者已恢复函数的调用栈。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值