pdf 深入理解kotlin协程_Kotlin协程实现原理:挂起与恢复

41a63d7616871c3d15bcb425b4e19f81.png

今天我们来聊聊Kotlin的协程Coroutine

如果你还没有接触过协程,推荐你先阅读这篇入门级文章What? 你还不知道Kotlin Coroutine?

如果你已经接触过协程,但对协程的原理存在疑惑,那么在阅读本篇文章之前推荐你先阅读下面的文章,这样能让你更全面更顺畅的理解这篇文章。

Kotlin协程实现原理:Suspend&CoroutineContext

Kotlin协程实现原理:CoroutineScope&Job

Kotlin协程实现原理:ContinuationInterceptor&CoroutineDispatcher

如果你已经接触过协程,相信你都有过以下几个疑问:

  1. 协程到底是个什么东西?
  2. 协程的suspend有什么作用,工作原理是怎样的?
  3. 协程中的一些关键名称(例如:JobCoroutineDispatcherCoroutineContextCoroutineScope)它们之间到底是怎么样的关系?
  4. 协程的所谓非阻塞式挂起与恢复又是什么?
  5. 协程的内部实现原理是怎么样的?
  6. ...

接下来的一些文章试着来分析一下这些疑问,也欢迎大家一起加入来讨论。

挂起

协程是使用非阻塞式挂起的方式来保证协程运行的。那么什么是非阻塞式挂起呢?下面我们来聊聊挂起到底是一个怎样的操作。

在之前的文章中提及到suspend关键字,它的一个作用是代码调用的时候会为方法添加一个Continuation类型的参数,保证协程中Continuaton的上下传递。

而它另一个关键作用是起到挂起协程的标识。

协程运行的时候每遇到被suspend修饰的方法时,都有可能会挂起当前的协程。

注意是有可能。

你可以随便写一个方法,该方法也可以被suspend修饰,但这种方法在协程中调用是不会被挂起的。例如

private suspend fun a() {println("aa")
}lifecycleScope.launch {a()
}

因为这种方法是不会返回COROUTINE_SUSPENDED类型的。

协程被挂起的标志是对应的状态下返回COROUTINE_SUSPENDED标识。

更深入一点的话就涉及到状态机。协程内部是使用状态机来管理协程的各个挂起点。

文字有点抽象,具体我们还是来看代码。我们就拿上面的a方法例子来说明。

首先在Android Studio打开这段代码的Kotlin Bytecode。可以在Tools -> Kotlin -> Show Kotlin Bytecode中打开。

然后点击其中的Decompile选项,生成对应的反编译java代码。最终代码如下:

BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {private CoroutineScope p$;Object L$0;int label;@Nullablepublic final Object invokeSuspend(@NotNull Object $result) {// 挂起标识Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();CoroutineScope $this$launch;switch(this.label) {case 0:ResultKt.throwOnFailure($result);$this$launch = this.p$;MainActivity var10000 = MainActivity.this;// 保存现场this.L$0 = $this$launch;// 设置挂起后恢复时,进入的状态this.label = 1;// 判断是否挂起if (var10000.a(this) == var3) {// 挂起,跳出该方法return var3;}// 不需要挂起,协程继续执行其他逻辑break;case 1:// 恢复现场$this$launch = (CoroutineScope)this.L$0;// 是否需要抛出异常ResultKt.throwOnFailure($result);break;default:throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");}return Unit.INSTANCE;}@NotNullpublic final Continuation create(@Nullable Object value, @NotNull Continuation completion) {Intrinsics.checkParameterIsNotNull(completion, "completion");Function2 var3 = new <anonymous constructor>(completion);var3.p$ = (CoroutineScope)value;return var3;}public final Object invoke(Object var1, Object var2) {return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);}
}), 3, (Object)null);

上面的代码就是协程的状态机,通过label来代表不同的状态,从而对应执行不同case中的逻辑代码。

在之前的文章中已经介绍过,协程启动的时候会手动调用一次resumeWith方法,而它对应的内部逻辑就是执行上面的invokeSuspend方法。

所以首次运行协程时label值为0,进入case 0:语句。此时会记录现场为可能被挂起的状态做准备,并设置下一个可能被执行的状态。

如果a方法的返回值为var3,这个var3对应的就是COROUTINE_SUSPENDED。所以只有当a方法返回COROUTINE_SUSPENDED时才会执行if内部语句,跳出方法,此时协程就被挂起。当前线程也就可以执行其它的逻辑,并不会被协程的挂起所阻塞。

所以协程的挂起在代码层面来说就是跳出协程执行的方法体,或者说跳出协程当前状态机下的对应状态,然后等待下一个状态来临时在进行执行。

那为什么说我们写的这个a方法不会被挂起呢?

@Nullable
final Object a(@NotNull Continuation $completion) {return Unit.INSTANCE;
}

原来是它的返回值并不是COROUTINE_SUSPENDED

既然它不会被挂起,那么什么情况下的方法才会被挂起呢?

很简单,如果我们在a方法中加入delay方法,它就会被挂起。

@Nullable
final Object a(@NotNull Continuation $completion) {Object var10000 = DelayKt.delay(1000L, $completion);return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}

真正触发挂起的是delay方法,因为delay方法会创建自己Continuation,同时内部调用getResult方法。

 internal fun getResult(): Any? {installParentCancellationHandler()if (trySuspend()) return COROUTINE_SUSPENDED// otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the stateval state = this.stateif (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)return getSuccessfulResult(state)}

getResult方法中会通过trySuspend来判断挂起当前协程。由挂起自身的协程,从而触发挂起父类的协程。

如果只是为了测试,可以让a方法直接返回COROUTINE_SUSPENDED

 private suspend fun a(): Any {return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED}

当然线上千万不能这样写,因为一旦这样写协程将一直被挂起,因为你没有将其恢复的能力。

恢复

现在我们再来聊一聊协程的恢复。

协程的恢复本质是通过ContinuationresumeWith方法来触发的。

下面我们来看一个可以挂起的例子,通过它来分析协程挂起与恢复的整个流程。

println("main start")
lifecycleScope.launch {println("async start")val b = async {delay(2000)"async"}b.await()println("async end")
}
Handler().postDelayed({println("main end")
}, 1000)

Kotlin代码很简单,当前协程运行与主线程中,内部执行一个async方法,通过await方法触发协程的挂起。

再来看它的对应反编译java代码

// 1
String var2 = "main start";
System.out.println(var2);
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {private CoroutineScope p$;Object L$0;Object L$1;int label;@Nullablepublic final Object invokeSuspend(@NotNull Object $result) {Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();CoroutineScope $this$launch;Deferred b;switch(this.label) {case 0:// 2ResultKt.throwOnFailure($result);$this$launch = this.p$;String var6 = "async start";System.out.println(var6);b = BuildersKt.async$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {private CoroutineScope p$;Object L$0;int label;@Nullablepublic final Object invokeSuspend(@NotNull Object $result) {Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();CoroutineScope $this$async;switch(this.label) {case 0:// 3ResultKt.throwOnFailure($result);$this$async = this.p$;this.L$0 = $this$async;this.label = 1;if (DelayKt.delay(2000L, this) == var3) {return var3;}break;case 1:// 5、6$this$async = (CoroutineScope)this.L$0;ResultKt.throwOnFailure($result);break;default:throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");}return "async";}@NotNullpublic final Continuation create(@Nullable Object value, @NotNull Continuation completion) {Intrinsics.checkParameterIsNotNull(completion, "completion");Function2 var3 = new <anonymous constructor>(completion);var3.p$ = (CoroutineScope)value;return var3;}public final Object invoke(Object var1, Object var2) {return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);}}), 3, (Object)null);this.L$0 = $this$launch;this.L$1 = b;this.label = 1;if (b.await(this) == var5) {return var5;}break;case 1:// 7b = (Deferred)this.L$1;$this$launch = (CoroutineScope)this.L$0;ResultKt.throwOnFailure($result);break;default:throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");}// 8String var4 = "async end";System.out.println(var4);return Unit.INSTANCE;}@NotNullpublic final Continuation create(@Nullable Object value, @NotNull Continuation completion) {Intrinsics.checkParameterIsNotNull(completion, "completion");Function2 var3 = new <anonymous constructor>(completion);var3.p$ = (CoroutineScope)value;return var3;}public final Object invoke(Object var1, Object var2) {return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);}
}), 3, (Object)null);
// 4
(new Handler()).postDelayed((Runnable)null.INSTANCE, 1000L);

有点长,没关系我们只看关键点,看它的状态机相关的内容。

  1. 首先会输出main start,然后通过launch创建协程,进入协程状态机,此时label0,执行case: 0相关逻辑。
  2. 进入case: 0输出async start,调用async并通过await来挂起当前协程,再挂起的过程中记录当前挂起点的数据,并将lable设置为1
  3. 进入async创建的协程,此时async协程中的lable0,进入async case: 0执行dealy并挂起async的协程。并将label设置为1。等待2s之后被唤醒。
  4. 此时协程都被挂起,即跳出协程launch方法,执行handler操作。由于post 1s所以比协程中dealy还短,所以会优先输出main end,然后再过1s,进入恢复协程阶段
  5. async中的协程被delay恢复,注意在delay方法中传入了thisasyncContinuation对象,所以delay内部一旦完成2s计时就会调用ContinuationresumeWith方法来恢复async中的协程,即调用invokeSuspend方法。
  6. 由于被挂起之前已经将async label设置为1,所以进入case: 1,恢复之前挂起的现场,检查异常,最终返回async
  7. 此时await挂起点被恢复,注意它也传入了this,对应的就是launch中的Continuation,所以也会回调resumeWith方法,最终调用invokeSuspend,即进入case 1:恢复现场,结束状态机。
  8. 最后再继续输出async end,协程运行结束。

我们可以执行上面的代码来验证输出是否正确

main start
async start
main end
async end

我们来总结一下,协程通过suspend来标识挂起点,但真正的挂起点还需要通过是否返回COROUTINE_SUSPENDED来判断,而代码体现是通过状态机来处理协程的挂起与恢复。在需要挂起的时候,先保留现场与设置下一个状态点,然后再通过退出方法的方式来挂起协程。在挂起的过程中并不会阻塞当前的线程。对应的恢复通过resumeWith来进入状态机的下一个状态,同时在进入下一个状态时会恢复之前挂起的现场。

本篇文章主要介绍了协程的挂起与恢复原理,同时也分析了协程的状态机相关的执行过程。希望对学习协程的伙伴们能够有所帮助,敬请期待后续的协程分析。

项目

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub: 基于Github客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于Jetpack&DataBindingMVVM;项目中使用了ArouterRetrofitCoroutineGlideDaggerHilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/258725.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

编译py-faster-rcnn的问题汇总及解决方法

按照官网 的提示&#xff0c;我开始安装faster rcnn&#xff0c;但是出现了很多问题&#xff0c;我将其汇总了起来&#xff0c;并提出了解决办法。 先说明一下我的配置&#xff1a; python : anaconda2linux: centos 6.9 安装faster rcnn请先参考&#xff1a;《cuda8cudnn4 F…

openWRT自学---针对backfire版本的主要目录和文件的作用的分析整理

特别说明&#xff1a;要编译backfire版本&#xff0c;一定要通过svn下载:svn co svn://svn.openwrt.org/openwrt/branches/backfire&#xff0c;而不能使用http://downloads.openwrt.org/backfire/10.03/中的源码包&#xff1a;backfire_10.03_source.tar.bz2 结合文档《OpenWr…

自然语言交流系统 phxnet团队 创新实训 项目博客 (五)

3DMax方面所涉及的专业知识&#xff1a; &#xff08;1&#xff09;一下的关于3DMax中对于人物的设计和操作均需要在对3DMax基础知识熟练掌握的情况下进行的。 &#xff08;2&#xff09;骨骼架设&#xff1a;首先对导入到3DMax中的人物模型进行架设骨骼…

linux 安装python-opencv

三种方法&#xff1a; 1. pip 安装 &#xff1a; pip install opencv-python &#xff0c;最新版为opencv3安装后>>> import cv2 >>> print cv2.__version__参考&#xff1a;http://www.cnblogs.com/lclblack/p/6377710.html 2. anaconda的conda安装 ,可以指…

《你的灯亮着吗》读书笔记Ⅲ

转载于:https://www.cnblogs.com/yue3475975/p/4586220.html

golang协程测试

package main import ( "fmt" "time") const NUMBER 1000000 func test() { for { }} func main() { fmt.Println(time.Now().UnixNano()) for i : 0; i < NUMBER; i { go test() } fmt.Println(time.Now().UnixNano()) for { }} 启动100W个协程&#…

nvidia显卡对比分析

本文章转载自&#xff1a;http://www.cnblogs.com/lijingcong/p/4958617.html 科学计算显卡的两个主要性能指标&#xff1a;1、CUDA compute capability&#xff0c;这是英伟达公司对显卡计算能力的一个衡量指标&#xff1b;2、FLOPS 每秒浮点运算次数&#xff0c;TFLOPS表示每…

零基础不建议学前端_web前端开发零基础怎样入门-哈尔滨前端学习

web前端开发零基础怎样入门-哈尔滨前端学习&#xff0c;俗话说&#xff0c;知己知彼&#xff0c;百战百胜。要想学好web前端&#xff0c;首先要了解什么是web前端&#xff0c;下面由小编来给大家介绍一下&#xff1a;1什么是web&#xff1f;Web就是在Http协议基础之上, 利用浏览…

描述项目的典型用户与场景

描述项目的典型用户与场景 名字&#xff1a;小威 年龄&#xff1a;22 职业&#xff1a;学生 收入&#xff1a;无正式收入 知识层次和能力&#xff1a;大学 生活/工作情况&#xff1a;卖东西赚外快 动机&#xff0c;目的&#xff0c;困难&#xff1a;卖东西东西时需要计数 用户比…

SpringBoot的配置项

2019独角兽企业重金招聘Python工程师标准>>> spring Boot 其默认是集成web容器的&#xff0c;启动方式由像普通Java程序一样&#xff0c;main函数入口启动。其内置Tomcat容器或Jetty容器&#xff0c;具体由配置来决定&#xff08;默认Tomcat&#xff09;。当然你也可…

北大OJ百练——4075:矩阵旋转(C语言)

百练的这道题很简单&#xff0c;通过率也达到了86%&#xff0c;所以我也就来贴个代码了。。。下面是题目&#xff1a; 不过还是说一下我的思路&#xff1a; 这道题对一个新来说&#xff0c;可能是会和矩阵的转置相混淆&#xff0c;这题并不是要我们去求矩阵的转置。 这题&#…

编译py-faster-rcnn全过程

编译py-faster-rcnn&#xff0c;花费了好几天&#xff0c;中间遇到好多问题&#xff0c;今天终于成功编译。下面详述我的整个编译过程。 【注记&#xff1a;】其实下面的依赖库可以安装在统一的一个本地目录下&#xff0c;相关安装指南&#xff0c;可以参考《深度学习&#xf…

翻译python语言命令_有道词典命令行快速翻译,Python编程的利器

本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。以下文章来源于Python实用宝典&#xff0c;作者Python实用宝典在编程时经常会遇到需要将中文词汇翻译成英文的情况。比如变量名的定义、取一个合适的函数…

不是世界不好,而是你见得太少

转载于:https://www.cnblogs.com/yymn/p/4590333.html

MonoBehaviour.FixedUpdate 固定更新

function FixedUpdate () : void Description描述 This function is called every fixed framerate frame, if the MonoBehaviour is enabled. 当MonoBehaviour启用时&#xff0c;其 FixedUpdate 在每一帧被调用。 FixedUpdate should be used instead of Update when dealing …

用Heartbeat实现web服务器高可用

用Heartbeat实现web服务器高可用heartbeat概述: Heartbeat 项目是 Linux-HA 工程的一个组成部分&#xff0c;它实现了一个高可用集群系统。心跳服务和集群通信是高可用集群的两个关键组件&#xff0c;在 Heartbeat 项目里&#xff0c;由 heartbeat 模块实现了这两个功能。端口号…

scp创建远程目录_在Linux系统中使用Vim读写远程文件

大家好&#xff0c;我是良许。 今天我们讨论一个 Vim 使用技巧——用 Vim 读写远程文件。要实现这个目的&#xff0c;我们需要使用到一个叫 netrw.vim 的插件。从 Vim 7.x 开始&#xff0c;netrw.vim 就被设置为默认安装的标准插件了。这个插件允许用户通过 ftp、rcp、scp、htt…

softmax logistic loss详解

softmax函数–softmax layer softmax用于多分类过程中&#xff0c;它将多个神经元的输出&#xff0c;映射到&#xff08;0,1&#xff09;区间内&#xff0c;可以看成概率来理解&#xff0c;从而来进行多分类&#xff01; 假设我们有一个数组z(z1,z2,...zm),则其softmax函数定…

poj3254 Corn Fields

Description Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and ca…

Android获取程序路径 (/data/data/appname)

Android获取文件夹路径 /data/data/ http://www.2cto.com/kf/201301/186614.html String printTxtPath getApplicationContext().getPackageResourcePath() "/files/" fileName;> /data/app/com.example.fileoperation-2.apk/files/printMenu.txt String print…