kotlin协程学习之三

协程的内部实现

有一类人不能接受只是开车,他们需要打开引擎盖来了解汽车的运作方式。我就是其中之一,所以我必须找出协程是如何工作的。如果你也是这样的人,你会喜欢这一章。如果不是,你可以跳过它。
这一章不会介绍任何您可能使用的新工具,它纯粹是解释性的。它试图以令人满意的程度解释协程的工作原理。关键教训是:

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

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

Continuation是可传递的

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


image.png

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

A very simple function

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


image.png

您已经可以推断出 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 设置为下一个状态,这样在恢复时就可以从挂起点后面的位置继续执行。这里的实际机制有一点更加复杂,因为标签的第一位也会被更改,并且这个更改会被暂停函数检查。这个机制是为了支持递归而需要的,但为了简单起见,先这么假设.


image.png

最后一个重要的部分已经在上面的代码片段中呈现出来了。当 delay 被挂起时,它会返回 COROUTINE_SUSPENDED,然后 myFunction 也会返回 COROUTINE_SUSPENDED;这个调用它的函数也会这样做,以及调用这个函数的函数,以及所有其他函数,一直到调用栈的顶部14。这是一个挂起如何结束所有这些函数,并将线程留给其他可运行项(包括协程)使用的方式。
在我们进一步探讨之前,让我们分析一下上面的代码。如果这个 delay 调用没有返回 COROUTINE_SUSPENDED 会发生什么?如果它只返回 Unit 而不是 COROUTINE_SUSPENDED 呢(我们知道它不会这样做,但是让我们假设)?请注意,如果 delay 只返回 Unit,我们将只是移动到下一个状态,并且该函数将像任何其他函数一样运行。
现在,让我们来谈谈 continuation,它是作为匿名类实现的。简化后,它看起来像这样:


image.png

当函数 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函数。

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


image.png

在异常处理上也是类似的:未捕获的异常在 resumeWith 中被捕获,然后被包装成 Result.failure(e),然后调用我们函数的函数会使用这个结果进行恢复。
希望这些内容能让你了解到当我们进行挂起时所发生的事情。状态需要存储在 continuation 中,并需要支持挂起机制。当我们恢复时,需要从 continuation 中恢复状态,并根据需要使用结果或抛出异常。
希望这些让你了解到了我们在暂停时所做的一切。需要在 continuation 中存储状态,并支持暂停机制。当我们恢复时,需要从 continuation 中恢复状态并使用结果或抛出异常。

The actual code

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

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

挂起函数的性能

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

结语

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

  • 挂起函数类似于状态机,有一个可能的状态在函数的开始和每个挂起函数调用后。
  • 标识状态的标签和本地数据都存储在continuation对象中。
  • 一个函数的 continuation 装饰着它的调用函数的 continuation,因此所有这些 continuation 代表了一个用于恢复或者已恢复函数的调用栈。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容