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

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

1. 前言



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


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


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


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


image.png

视频链接👉www.youtube.com/watch?v=Yrr…

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


2. 一个循序渐进的例子


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


2.1 直接在主线程调用



image.png

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


2.2 使用callback


image.png

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


2.3 使用callback和handler组合

image.png

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


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


1. 线程或者线程池技术

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

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



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


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

  1. Dispatchers.Main、Dispatchers.IO等对应线程
  2. Continuation对应线程中的回调
  3. DispatchedContinuation同时指定了Continuation回调,又指定了回调在哪个线程中执行。

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


2.4 使用协程实现


image.png

image.png


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


3. 魔法揭秘



首先贴一下完整版代码


image.png

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

image.png

image.png

image.png

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


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


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

image.png


4. 研究launch反编译



image.png

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


  1. lambda表达式被转换成了Function2实例,那么Function2是什么?为什么又给Function2传了一个类型为Continuation的null对象?
  2. invokeSuspend方法是干嘛的?注意参数是非空的。
  3. 调用heavyWork(this)传入了this对象。从前文我们可以看到,heavyWork方法反编译之后变成了heavyWork(Continuation var)。那么说明Function2是Continuation类型。


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


4.1 invokeSuspend


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


image.png

image.png

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

image.png

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

image.png

image.png

image.png

  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的子类。


image.png

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,以记录回调要在哪个线程调用。


image.png

image.png

image.png


相关文章
|
3月前
|
存储 Linux 调度
协程(coroutine)的原理和使用
协程(coroutine)的原理和使用
|
17天前
|
存储 安全 测试技术
GoLang协程Goroutiney原理与GMP模型详解
本文详细介绍了Go语言中的Goroutine及其背后的GMP模型。Goroutine是Go语言中的一种轻量级线程,由Go运行时管理,支持高效的并发编程。文章讲解了Goroutine的创建、调度、上下文切换和栈管理等核心机制,并通过示例代码展示了如何使用Goroutine。GMP模型(Goroutine、Processor、Machine)是Go运行时调度Goroutine的基础,通过合理的调度策略,实现了高并发和高性能的程序执行。
77 29
|
15天前
|
负载均衡 算法 Go
GoLang协程Goroutiney原理与GMP模型详解
【11月更文挑战第4天】Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理,创建和销毁开销小,适合高并发场景。其调度采用非抢占式和协作式多任务处理结合的方式。GMP 模型包括 G(Goroutine)、M(系统线程)和 P(逻辑处理器),通过工作窃取算法实现负载均衡,确保高效利用系统资源。
|
2月前
|
调度 Python
揭秘Python并发编程核心:深入理解协程与异步函数的工作原理
在Python异步编程领域,协程与异步函数成为处理并发任务的关键工具。协程(微线程)比操作系统线程更轻量级,通过`async def`定义并在遇到`await`表达式时暂停执行。异步函数利用`await`实现任务间的切换。事件循环作为异步编程的核心,负责调度任务;`asyncio`库提供了事件循环的管理。Future对象则优雅地处理异步结果。掌握这些概念,可使代码更高效、简洁且易于维护。
26 1
|
1月前
|
数据采集 调度 Python
Python编程异步爬虫——协程的基本原理(一)
Python编程异步爬虫——协程的基本原理(一)
|
1月前
|
数据采集 Python
Python编程异步爬虫——协程的基本原理(二)
Python编程异步爬虫——协程的基本原理(二)
|
1月前
|
存储 前端开发 rax
协程设计与原理(二)
协程设计与原理(二)
16 0
|
1月前
|
Java Linux Go
协程的设计原理(一)
协程的设计原理(一)
32 0
|
4月前
|
调度 Python
揭秘Python并发编程核心:深入理解协程与异步函数的工作原理
【7月更文挑战第15天】Python异步编程借助协程和async/await提升并发性能,减少资源消耗。协程(async def)轻量级、用户态,便于控制。事件循环,如`asyncio.get_event_loop()`,调度任务执行。异步函数内的await关键词用于协程间切换。回调和Future对象简化异步结果处理。理解这些概念能写出高效、易维护的异步代码。
58 2
|
6月前
|
存储 关系型数据库 MySQL
纯c协程框架NtyCo实现与原理
纯c协程框架NtyCo实现与原理
153 1