今天我们来聊聊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: 每日一算法,由浅入深,欢迎加入一起共勉。

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

  1. pdf 深入理解kotlin协程_协程初探

    Hello,各位朋友,小笨鸟我回来了! 近期学习了Kotlin协程相关的知识,感觉这块技术在项目中的可应用性很大,对项目的开发效率和维护成本有较大的提升.于是就考虑深入研究下相关概念和使用方式,并引入 ...

  2. kotlin协程_Kotlin协程

    kotlin协程 In this tutorial, we'll be looking into Kotlin Coroutines. Coroutines is a vital concept si ...

  3. pdf 深入理解kotlin协程_深入理解Kotlin协程

    领取成功 您已领取成功! 您可以进入Android/iOS/Kindle平台的多看阅读客户端,刷新个人中心的已购列表,即可下载图书,享受精品阅读时光啦! - | 回复不要太快哦~ 回复内容不能为空哦 ...

  4. 分析Kotlin协程只挂起不恢复会怎样(是否存在协程泄漏),以及挂起的协程存在哪里?

    前言 刚开始正式学协程原理的时候(以前只是学api怎么用),大概是20年6月,也就是bennyhuo大佬出书<深入理解Kotlin协程>的时候,我买了本然后细细研究,我的内心就一直有一个问 ...

  5. 协程的挂起、恢复和调度的原理 (二)

    目录 一. 协程的挂起.恢复和调度的设计思想 二. 深入解析协程 1. 协程的创建与启动 2. 协程的线程调度 3. 协程的挂起和恢复 4. 不同 resumeWith 的解析 5. 协程整体结构 一 ...

  6. 深入理解Kotlin协程suspend工作原理(初学者也能看得懂)

    1. 概述 挂起函数是Kotlin协程最重要的一个特性,所有其他概念都建立在它的基础上.所以我们需要深入了解它的工作原理. 挂起协程意味着在中间停止它.这类似于玩游戏,当我们想暂停游戏时,可以先存档, ...

  7. Kotlin学习笔记26 协程part6 协程与线程的关系 Dispatchers.Unconfined 协程调试 协程上下文切换 Job详解 父子协程的关系

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 协程与线程的关系 import kotlinx.coroutines.* import java.util.concu ...

  8. Kotlin学习笔记24 协程part4 协程的取消与超时

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 如何取消协程 import kotlinx.coroutines.*/*** 协程的取消*/fun main() = ...

  9. Kotlin学习笔记21 协程part1 基本概念

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 本节先介绍协程的相关概念 概念可能枯燥,我们先要了解协程中的相关概念 然后结合代码理解这些概念 加深印象 协程的定义 协程 ...

最新文章

  1. 做项目经理到底有多爽?
  2. visual studio怎么重启?(visual studio restart插件)
  3. 文件加解密,文件操作
  4. 为了追到小姐姐,我用 Python 制作了一个机器人
  5. 《天天数学》连载27:一月二十七日
  6. php 呼叫中心 源码,FreeSWITCH+Workerman+PHP 搭建呼叫中心
  7. ZYNQ ZCU102视频编码开发
  8. 【GPT-3】除了缺少点创意,GPT-3写出了及格的大学毕业论文,只需20分钟
  9. 阿里巴巴 CTO 首次分享技术战略
  10. 如何配置mysql_怎样配置MySQL
  11. Java性能优化的七个方向
  12. Ubuntu操作系统的学习,新手上路
  13. Windows图片和传真查看器开启故障
  14. 人睡眠时做恶梦以及梦魇或鬼压身的原因
  15. 查看各大网站服务器操作系统
  16. 关于Web前端div中<p>等块状标签位置问题
  17. 实现苹果和虫子(c语言)
  18. ensp防火墙出口路由双链路运营商,负载分担及设备冗余
  19. GIC 基础知识介绍 (一)
  20. 美赛论文Latex简易模板 | 快速上手(附注释)

热门文章

  1. java文件读写操作指定编码格式[转]
  2. 当Grid的数据源是View时,如何使用PeopleSoft自带的Save Function?
  3. 好消息:VS 2008 and .NET 3.5 Beta 2 发布了
  4. Swift傻傻分不清楚系列(九)闭包
  5. 002 模板实参推断、重载与模板
  6. JS脚本显示当前日期+星期几[转]
  7. 去掉xcode中警告的一些经验
  8. 【Oracle 学习笔记】Day 1 常用函数整理(转换、DeCode),表的外键
  9. TreeView控件二(递归算法)
  10. css --- 兄弟选择器