抽丝剥茧聊协程之深入理解Continuation原理

1. 前言

这是新年以来的第一篇更文,在此给大家拜个晚年,祝大家在新的一年所想的都能如愿,同时感谢大家一直以来的支持和帮助。这篇文章其实在春节前就已经构思完了,本想着在留京过年期间写完,由于计划变更,回老家过年去了,春节期间大部分时间在走亲戚,文章也就搁置下来了。

闲话少叙,本文我将尝试给大家讲明白,Kotlin协程如何实现用同步的方式实现异步调用。相信不少同学都能说出以下几种概念中的一个或者多个。

  • Kotlin suspend关键字
  • Kotlin内部的Continuation机制
  • Continuation Passing Style (CPS)机制
  • 有限状态机机制

如果你看过Kotlin协程架构师的Deep dive into Coroutines on JVM演讲视频,相信你对上面的概念不会陌生。

视频链接👉https://www.youtube.com/watch?v=YrrUCSi72E8&t=1329s

但是如果你没有深入源码探究其实现原理,相信你对上面的概念也是一知半解。本文将带领大家更加详细地了解这一知识。

2. 一个循序渐进的例子

假设有这样一个简单的场景,在App上发起一个请求,10s后拿到响应,更新到用户界面上。我们可能会遇到以下几种写法。

2.1 直接在主线程调用


这种情况,大家都很清楚,对于客户端开发,在主线程执行耗时操作这是万万不可的。为了不阻塞主线程,我们需要通过开启新线程,使用回调的方式,将结果回传过来。

2.2 使用callback


这种方式,虽然解决了在主线程做耗时操作的问题,但是引入了新问题,在子线程更新UI,会导致应用崩溃。为了解决这个问题,我们需要通过主线程Handler,把callback post到主线程运行。

2.3 使用callback和handler组合

目前为止,我们通过线程,回调,Handler组合方式,完美地实现了执行一个异步请求,并将结果更新到UI上。

在这个方案中有三个要素:

1. 线程或者线程池技术

2. 回调机制,将结果反馈给调用者

3. 回调操作在哪个线程中执行


那么此处应该敲黑板划重点了

既然本文是讲协程,那么协程中哪些类分别对应这三个要素呢?

  1. Dispatchers.Main、Dispatchers.IO等对应线程

  2. Continuation对应线程中的回调

  3. DispatchedContinuation同时指定了Continuation回调,又指定了回调在哪个线程中执行。

在此先不展开讲解Continuation和DispatchedContinuation的具体细节,此处做个铺垫,后文会详细讲解。

2.4 使用协程实现

该代码是协程实现的正确代码,没有主线程执行耗时操作问题,没有子线程更新UI问题。当然也没有显式的callback,和线程切换的代码。只对suspendHeavyWork方法增加了suspend关键字,就能将异步的调用 用同步的代码实现。那么魔法在哪呢?

3. 魔法揭秘

首先贴一下完整版代码

使用Android Studio的Show Kotlin Bytecode功能查看反编译后的文件

感慨x1 看了反编译后的代码,可能需要感慨一下,哪有什么岁月静好,只不过有人在替你负重前行。用协程写代码爽,是因为编译器在后面做了不少工作,理解这背后的工作,对我们使用协程大有裨益。

困惑x1 那么编译器生成的这些代码,看起来既熟悉又陌生,说熟悉是因为就语法而言每行代码都认识(switch case都很熟悉),说陌生是因为总体而言,不太明白他们干了什么,甚至有的函数(比如invokeSuspend)连在哪调用的都不知道。

困惑x2 更让人困惑的是,状态机代码中明明出现了return语句,请问后续的代码是如何执行的?要搞清楚状态机的原理必须搞清楚这个问题。

4. 研究launch反编译


上图标出了三个重要的地方。

  1. lambda表达式被转换成了Function2实例,那么Function2是什么?为什么又给Function2传了一个类型为Continuation的null对象?

  2. invokeSuspend方法是干嘛的?注意参数是非空的。

  3. 调用heavyWork(this)传入了this对象。从前文我们可以看到,heavyWork方法反编译之后变成了heavyWork(Continuation var)。那么说明Function2是Continuation类型。

有了以上几个问题,那么离揭晓答案也就不远了。

4.1 invokeSuspend

首先回答最简单的那个问题,invokeSuspend方法是干嘛的。它定义在BaseContinuationImpl类中,是一个抽象方法。

从BaseContinuationImpl(public val completion:Continuation<Any?>?)我们可以知道协程中的回调是用链表串起来的。

假设有suspend函数调用如下,那么Continuation关系图如下。

  1. 首先一个while循环,它保证了状态机能够轮询。
  2. val completion = completion!! 如果当前Continuation 左边没有回调了,快速返回
  3. val outcome = invokeSuspend(param) 调用当前Continuation的 invokeSuspend方法
  4. 如果outcome === COROUTINE_SUSPENDED直接返回。这就是为什么delay方法不会阻塞当前线程的原因,遇到suspend方法 label会+1,当前Continuation会传递给delay。
  5. if (completion is BaseContinuationImpl)继续递归调用invokeSuspend
  6. 否则调用completion.resumeWith(outcome)并且返回

这段代码就是保证状态机能够运行的核心。

4.2 Function2是什么?

Function2是SuspendLambda,定义在ContinuationImpl.kt中。它是BaseContinuationImpl的子类。

5. DispatchedContinuation

internal class DispatchedContinuation<in T>(
    @JvmField val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>
)

DispatchedContinuation 有dispatcher和continuation两个成员变量。表示在dispatcher所对应的线程,执行continuation回调。在发生线程切换时,一定会生成DispatchedContinuation对象,否则,切完线程后,就无法再切回来了。

例如 delay和withContext,切换线程后都会创建DispatchedContinuation,以记录回调要在哪个线程调用。


欢迎关注"字节小站"同名公众号!!
欢迎关注"字节小站"同名公众号!!
欢迎关注"字节小站"同名公众号!!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin协程是一种轻量级的并发框架,它可以在不创建新线程的情况下实现异步操作。Kotlin协程的实现原理是基于挂起函数和Continuation协程上下文)。 在Kotlin协程中,挂起函数是指可以被暂停执行,并在稍后继续执行的函数。在挂起函数中,可以使用`suspend`关键字来标记该函数为挂起函数。当调用一个挂起函数时,该函数会返回一个`Continuation`对象,该对象可以被用来在稍后的时间点恢复挂起函数的执行。 Kotlin协程的调度器会在适当的时候调用Continuation对象的`resume`方法来恢复挂起函数的执行。当一个挂起函数被恢复执行时,它会从上一次挂起的地方继续执行,直到函数结束或者再次遇到挂起点。 Kotlin协程的实现原理可以用以下伪代码来说明: ```kotlin fun main() { launch { println("Hello") delay(1000) println("World") } } suspend fun delay(time: Long) { // 挂起当前协程,等待一段时间 // 通过Continuation对象来恢复协程的执行 suspendCoroutine<Unit> { continuation -> Timer().schedule(time) { continuation.resume(Unit) } } } fun launch(block: suspend () -> Unit) { // 创建一个新的协程,并将其加入到调度器中 val coroutine = Coroutine(block) coroutine.start() } class Coroutine(private val block: suspend () -> Unit) { fun start() { block.startCoroutine(this) } } class Continuation(private val coroutine: Coroutine) { fun resume(value: Any?) { coroutine.resume(value) } } suspend fun <T> suspendCoroutine(block: (Continuation<T>) -> Unit): T { // 挂起当前协程,等待Continuation对象被调用 // 通过Continuation对象来恢复协程的执行 return suspendCoroutineOrReturn { continuation -> block(Continuation(coroutine)) } } ``` 在上面的代码中,`launch`函数用于创建一个新的协程,并将其加入到调度器中。`Coroutine`类表示一个协程,`start`方法用于启动协程的执行。`suspendCoroutine`函数用于挂起当前协程,并等待Continuation对象被调用以恢复协程的执行。`delay`函数使用`suspendCoroutine`函数来实现挂起功能,并在一定时间后恢复协程的执行。 Kotlin协程的实现原理比较复杂,但是开发者只需要关注如何使用协程来实现异步操作即可。通过使用协程,开发者可以编写出更加简洁、易于理解、易于维护的异步代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值