关键词:Kotlin 协程 协程挂起 任务挂起 suspend 非阻塞

协程的挂起最初是一个很神秘的东西,因为我们总是用线程的概念去思考,所以我们只能想到阻塞。不阻塞的挂起到底是怎么回事呢?说出来你也许会笑~~(哭?。。抱歉这篇文章我实在是没办法写的更通俗易懂了,大家一定要亲手实践!)

1. 先看看 delay

我们刚刚学线程的时候,最常见的模拟各种延时用的就是 Thread.sleep 了,而在协程里面,对应的就是 delaysleep 让线程进入休眠状态,直到指定时间之后某种信号或者条件到达,线程就尝试恢复执行,而 delay 会让协程挂起,这个过程并不会阻塞 CPU,甚至可以说从硬件使用效率上来讲是“什么都不耽误”,从这个意义上讲 delay 也可以是让协程休眠的一种很好的手段。

delay 的源码其实很简单:

  1. public suspend fun delay(timeMillis: Long) {

  2. if (timeMillis <= 0) return // don't delay

  3. return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->

  4. cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)

  5. }

  6. }

cont.context.delay.scheduleResumeAfterDelay 这个操作,你可以类比 JavaScript 的 setTimeout,Android 的 handler.postDelay,本质上就是设置了一个延时回调,时间一到就调用 cont 的 resume 系列方法让协程继续执行。

剩下的最关键的就是 suspendCancellableCoroutine 了,这可是我们的老朋友了,前面我们用它实现了回调到协程的各种转换 —— 原来 delay 也是基于它实现的,如果我们再多看一些源码,你就会发现类似的还有 joinawait 等等。

2. 再来说说 suspendCancellableCoroutine

既然大家对于 suspendCancellableCoroutine 已经很熟悉了,那么我们干脆直接召唤一个老朋友给大家:

  1. private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->

  2. cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(this, cont).asHandler))

  3. }

Job.join() 这个方法会首先检查调用者 Job 的状态是否已经完成,如果是,就直接返回并继续执行后面的代码而不再挂起,否则就会走到这个 joinSuspend 的分支当中。我们看到这里只是注册了一个完成时的回调,那么传说中的 suspendCancellableCoroutine 内部究竟做了什么呢?

  1. public suspend inline fun <T> suspendCancellableCoroutine(

  2. crossinline block: (CancellableContinuation<T>) -> Unit

  3. ): T =

  4. suspendCoroutineUninterceptedOrReturn { uCont ->

  5. val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)

  6. block(cancellable)

  7. cancellable.getResult() // 这里的类型是 Any?

  8. }

suspendCoroutineUninterceptedOrReturn 这个方法调用的源码是看不到的,因为它根本没有源码:P 它的逻辑就是帮大家拿到 Continuation 实例,真的就只有这样。不过这样说起来还是很抽象,因为有一处非常的可疑: suspendCoroutineUninterceptedOrReturn 的返回值类型是 T,而传入的 lambda 的返回值类型是 Any?, 也就是我们看到的 cancellable.getResult() 的类型是 Any?,这是为什么?

我记得在协程系列文章的开篇,我就提到过 suspend 函数的签名,当时是以 await 为例的,这个方法大致相当于:

  1. fun await(continuation: Continuation<User>): Any {

  2. ...

  3. }

suspend 一方面为这个方法添加了一个 Continuation 的参数,另一方面,原先的返回值类型 User 成了 Continuation 的泛型实参,而真正的返回值类型竟然是 Any。当然,这里因为定义的逻辑返回值类型 User是不可空的,因此真实的返回值类型也用了 Any 来示意,如果泛型实参是个可空的类型,那么真实的返回值类型也就是 Any? 了,这正与前面提到的 cancellable.getResult() 返回的这个 Any? 相对应。

如果大家去查 await 的源码,你同样会看到这个 getResult() 的调用。

简单来说就是,对于 suspend 函数,不是一定要挂起的,可以在需要的时候挂起,也就是要等待的协程还没有执行完的时候,等待协程执行完再继续执行;而如果在开始 join 或者 await 或者其他 suspend 函数,如果目标协程已经完成,那么就没必要等了,直接拿着结果走人即可。那么这个神奇的逻辑就在于 cancellable.getResult() 究竟返回什么了,且看:

  1. internal fun getResult(): Any? {

  2. ...

  3. if (trySuspend()) return COROUTINE_SUSPENDED // ① 触发挂起逻辑

  4. ...

  5. if (state is CompletedExceptionally) // ② 异常立即抛出

  6. throw recoverStackTrace(state.cause, this)

  7. return getSuccessfulResult(state) // ③ 正常结果立即返回

  8. }

这段代码 ① 处就是挂起逻辑了,表示这时候目标协程还没有执行完,需要等待结果,②③是协程已经执行完可以直接拿到异常和正常结果的两种情况。②③好理解,关键是 ①,它要挂起,这返回的是个什么东西?

  1. public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED

  2. internal enum class CoroutineSingletons { COROUTINE_SUSPENDED, UNDECIDED, RESUMED }

这是 1.3 的实现,1.3 以前的实现更有趣,就是一个白板 Any。其实是什么不重要,关键是这个东西是一个单例,任何时候协程见到它就知道自己该挂起了。

3. 深入挂起操作

既然说到挂起,大家可能觉得还是一知半解,还是不知道挂起究竟怎么做到的,怎么办?说真的这个挂起是个什么操作其实一直没有拿出来给大家看,不是我们太小气了,只是太早拿出来会比较吓人。。

  1. suspend fun hello() = suspendCoroutineUninterceptedOrReturn<Int>{

  2. continuation ->

  3. log(1)

  4. thread {

  5. Thread.sleep(1000)

  6. log(2)

  7. continuation.resume(1024)

  8. }

  9. log(3)

  10. COROUTINE_SUSPENDED

  11. }

我写了这么一个 suspend 函数,在 suspendCoroutineUninterceptedOrReturn 当中直接返回了这个传说中的白板 COROUTINE_SUSPENDED,正常来说我们应该在一个协程当中调用这个方法对吧,可是我偏不,我写一段 Java 代码去调用这个方法,结果会怎样呢?

  1. public class CallCoroutine {

  2. public static void main(String... args) {

  3. Object value = SuspendTestKt.hello(new Continuation<Integer>() {

  4. @NotNull

  5. @Override

  6. public CoroutineContext getContext() {

  7. return EmptyCoroutineContext.INSTANCE;

  8. }

  9. @Override

  10. public void resumeWith(@NotNull Object o) { // ①

  11. if(o instanceof Integer){

  12. handleResult(o);

  13. } else {

  14. Throwable throwable = (Throwable) o;

  15. throwable.printStackTrace();

  16. }

  17. }

  18. });

  19. if(value == IntrinsicsKt.getCOROUTINE_SUSPENDED()){ // ②

  20. LogKt.log("Suspended.");

  21. } else {

  22. handleResult(value);

  23. }

  24. }

  25. public static void handleResult(Object o){

  26. LogKt.log("The result is " + o);

  27. }

  28. }

这段代码看上去比较奇怪,可能会让人困惑的有两处:

① 处,我们在 Kotlin 当中看到的 resumeWith 的参数类型是 Result,怎么这儿成了 Object 了?因为 Result 是内联类,编译时会用它唯一的成员替换掉它,因此就替换成了 Object (在Kotlin 里面是 Any?

② 处 IntrinsicsKt.getCOROUTINE_SUSPENDED() 就是 Kotlin 的 COROUTINE_SUSPENDED

剩下的其实并不难理解,运行结果自然就是如下所示了:

  1. 07:52:55:288 [main] 1

  2. 07:52:55:293 [main] 3

  3. 07:52:55:296 [main] Suspended.

  4. 07:52:56:298 [Thread-0] 2

  5. 07:52:56:306 [Thread-0] The result is 1024

其实这段 Java 代码的调用方式与 Kotlin 下面的调用已经很接近了:

  1. suspend fun main() {

  2. log(hello())

  3. }

只不过我们在 Kotlin 当中还是不太容易拿到 hello 在挂起时的真正返回值,其他的返回结果完全相同。

  1. 12:44:08:290 [main] 1

  2. 12:44:08:292 [main] 3

  3. 12:44:09:296 [Thread-0] 2

  4. 12:44:09:296 [Thread-0] 1024

很有可能你看到这里都会觉得晕头转向,没有关系,我现在已经开始尝试揭示一些协程挂起的背后逻辑了,比起简单的使用,概念的理解和接受需要有个小小的过程。

4. 深入理解协程的状态转移

前面我们已经对协程的原理做了一些揭示,显然 Java 的代码让大家能够更容易理解,那么接下来我们再来看一个更复杂的例子:

  1. suspend fun returnSuspended() = suspendCoroutineUninterceptedOrReturn<String>{

  2. continuation ->

  3. thread {

  4. Thread.sleep(1000)

  5. continuation.resume("Return suspended.")

  6. }

  7. COROUTINE_SUSPENDED

  8. }

  9. suspend fun returnImmediately() = suspendCoroutineUninterceptedOrReturn<String>{

  10. log(1)

  11. "Return immediately."

  12. }

我们首先定义两个挂起函数,第一个会真正挂起,第二个则会直接返回结果,这类似于我们前面讨论 join 或者 await 的两条路径。我们再用 Kotlin 给出一个调用它们的例子:

  1. suspend fun main() {

  2. log(1)

  3. log(returnSuspended())

  4. log(2)

  5. delay(1000)

  6. log(3)

  7. log(returnImmediately())

  8. log(4)

  9. }

运行结果如下:

  1. 08:09:37:090 [main] 1

  2. 08:09:38:096 [Thread-0] Return suspended.

  3. 08:09:38:096 [Thread-0] 2

  4. 08:09:39:141 [kotlinx.coroutines.DefaultExecutor] 3

  5. 08:09:39:141 [kotlinx.coroutines.DefaultExecutor] Return immediately.

  6. 08:09:39:141 [kotlinx.coroutines.DefaultExecutor] 4

好,现在我们要揭示这段协程代码的真实面貌,为了做到这一点,我们用 Java 来仿写一下这段逻辑:

注意,下面的代码逻辑上并不能做到十分严谨,不应该出现在生产当中,仅供学习理解协程使用。

  1. public class ContinuationImpl implements Continuation<Object> {

  2. private int label = 0;

  3. private final Continuation<Unit> completion;

  4. public ContinuationImpl(Continuation<Unit> completion) {

  5. this.completion = completion;

  6. }

  7. @Override

  8. public CoroutineContext getContext() {

  9. return EmptyCoroutineContext.INSTANCE;

  10. }

  11. @Override

  12. public void resumeWith(@NotNull Object o) {

  13. try {

  14. Object result = o;

  15. switch (label) {

  16. case 0: {

  17. LogKt.log(1);

  18. result = SuspendFunctionsKt.returnSuspended( this);

  19. label++;

  20. if (isSuspended(result)) return;

  21. }

  22. case 1: {

  23. LogKt.log(result);

  24. LogKt.log(2);

  25. result = DelayKt.delay(1000, this);

  26. label++;

  27. if (isSuspended(result)) return;

  28. }

  29. case 2: {

  30. LogKt.log(3);

  31. result = SuspendFunctionsKt.returnImmediately( this);

  32. label++;

  33. if (isSuspended(result)) return;

  34. }

  35. case 3:{

  36. LogKt.log(result);

  37. LogKt.log(4);

  38. }

  39. }

  40. completion.resumeWith(Unit.INSTANCE);

  41. } catch (Exception e) {

  42. completion.resumeWith(e);

  43. }

  44. }

  45. private boolean isSuspended(Object result) {

  46. return result == IntrinsicsKt.getCOROUTINE_SUSPENDED();

  47. }

  48. }

我们定义了一个 Java 类 ContinuationImpl,它就是一个 Continuation 的实现。

实际上如果你愿意,你还真得可以在 Kotlin 的标准库当中找到一个名叫 ContinuationImpl 的类,只不过,它的 resumeWith 最终调用到了 invokeSuspend,而这个 invokeSuspend 实际上就是我们的协程体,通常也就是一个 Lambda 表达式 —— 我们通过 launch启动协程,传入的那个 Lambda 表达式,实际上会被编译成一个 SuspendLambda 的子类,而它又是 ContinuationImpl 的子类。

有了这个类我们还需要准备一个 completion 用来接收结果,这个类仿照标准库的 RunSuspend 类实现,如果你有阅读前面的文章,那么你应该知道 suspend main 的实现就是基于这个类:

  1. public class RunSuspend implements Continuation<Unit> {

  2. private Object result;

  3. @Override

  4. public CoroutineContext getContext() {

  5. return EmptyCoroutineContext.INSTANCE;

  6. }

  7. @Override

  8. public void resumeWith(@NotNull Object result) {

  9. synchronized (this){

  10. this.result = result;

  11. notifyAll(); // 协程已经结束,通知下面的 wait() 方法停止阻塞

  12. }

  13. }

  14. public void await() throws Throwable {

  15. synchronized (this){

  16. while (true){

  17. Object result = this.result;

  18. if(result == null) wait(); // 调用了 Object.wait(),阻塞当前线程,在 notify 或者 notifyAll 调用时返回

  19. else if(result instanceof Throwable){

  20. throw (Throwable) result;

  21. } else return;

  22. }

  23. }

  24. }

  25. }

这段代码的关键点在于 await() 方法,它在其中起了一个死循环,不过大家不要害怕,这个死循环是个纸老虎,如果 resultnull,那么当前线程会被立即阻塞,直到结果出现。具体的使用方法如下:

  1. ...

  2. public static void main(String... args) throws Throwable {

  3. RunSuspend runSuspend = new RunSuspend();

  4. ContinuationImpl table = new ContinuationImpl(runSuspend);

  5. table.resumeWith(Unit.INSTANCE);

  6. runSuspend.await();

  7. }

  8. ...

这写法简直就是 suspend main 的真实面貌了。

我们看到,作为 completion 传入的 RunSuspend 实例的 resumeWith 实际上是在 ContinuationImplresumeWtih 的最后才会被调用,因此它的 await() 一旦进入阻塞态,直到 ContinuationImpl 的整体状态流转完毕才会停止阻塞,此时进程也就运行完毕正常退出了。

于是这段代码的运行结果如下:

  1. 08:36:51:305 [main] 1

  2. 08:36:52:315 [Thread-0] Return suspended.

  3. 08:36:52:315 [Thread-0] 2

  4. 08:36:53:362 [kotlinx.coroutines.DefaultExecutor] 3

  5. 08:36:53:362 [kotlinx.coroutines.DefaultExecutor] Return immediately.

  6. 08:36:53:362 [kotlinx.coroutines.DefaultExecutor] 4

我们看到,这段普通的 Java 代码与前面的 Kotlin 协程调用完全一样。那么我这段 Java 代码的编写根据是什么呢?就是 Kotlin 协程编译之后产生的字节码。当然,字节码是比较抽象的,我这样写出来就是为了让大家更容易的理解协程是如何执行的,看到这里,相信大家对于协程的本质有了进一步的认识:

  • 协程的挂起函数本质上就是一个回调,回调类型就是 Continuation

  • 协程体的执行就是一个状态机,每一次遇到挂起函数,都是一次状态转移,就像我们前面例子中的 label 不断的自增来实现状态流转一样

如果能够把这两点认识清楚,那么相信你在学习协程其他概念的时候就都将不再是问题了。如果想要进行线程调度,就按照我们讲到的调度器的做法,在 resumeWith 处执行线程切换就好了,其实非常容易理解的。官方的协程框架本质上就是在做这么几件事儿,如果你去看源码,可能一时云里雾里,主要是因为框架除了实现核心逻辑外还需要考虑跨平台实现,还需要优化性能,但不管怎么样,这源码横竖看起来就是五个字:状态机回调。

5. 小结

不同以往,我们从这一篇开始毫无保留的为大家尝试揭示协程背后的逻辑,也许一时间可能有些难懂,不过不要紧,你可以使用协程一段时间之后再来阅读这些内容,相信一定会豁然开朗的。

当然,这一篇内容的安排更多是为后面的序列篇开路,Kotlin 的 Sequence 就是基于协程实现的,它的用法很简单,几乎与普通的 Iterable 没什么区别,因此序列篇我们会重点关注它的内部实现原理,欢迎大家关注。

另外,想要找到好 Offer、想要实现技术进阶的迷茫中的 Android 工程师们,推荐大家关注下我的新课《破解Android高级面试》,这门课已经更新完毕,涉及内容均非浅尝辄止,目前已经有300+同学在学习,你还在等什么(*≧∪≦):

长按识别二维码即可进入课程啦!

破解 Kotlin 协程(6) - 协程挂起篇相关推荐

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

    今天我们来聊聊Kotlin的协程Coroutine. 如果你还没有接触过协程,推荐你先阅读这篇入门级文章What? 你还不知道Kotlin Coroutine? 如果你已经接触过协程,但对协程的原理存 ...

  2. Kotlin学习笔记23 协程part3 lambda表达式深入 挂起函数 全局协程

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 lambda表达式深入 /*** lambda 表达式深入* 当函数参数是函数时 并且该函数只有一个参数 可以不传入任 ...

  3. 破解Kotlin协程创建调用的那些事

    Kotlin协程从1.3正式版除出来也很久了,相比大家伙也比较熟悉了,从Android的AAC架构到后后端都可以见到它的身影,那么问题来了,用了那么久的协程体你知道它怎么创建的么. 一天我问同事:你知 ...

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

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

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

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

  6. Kotlin学习笔记22 协程part2 join CoroutineScope 协程vs线程

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 Job的join方法 import kotlinx.coroutines.* /*** Job的join方法* 它会挂 ...

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

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

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

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

  9. kotlin协程_Kotlin协程

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

  10. 《Kotlin 程序设计》第十二章 Kotlin的多线程:协程(Coroutines)

    第十二章 Kotlin的多线程:协程(Coroutines) Kotlin 1.1 introduced coroutines, a new way of writing asynchronous, ...

最新文章

  1. selenium java 验证码_如何使用Selenium WebDriver和Java从图像(验证码)中读取文本
  2. 让我们一起Go(十三)
  3. ACM/ICPC 2018亚洲区预选赛北京赛站网络赛 80 Days(双向队列+尺取法)
  4. android 自定义菜单开发,Android开发学习笔记:浅谈3大类菜单
  5. 二叉搜索时与双向链表python_【剑指offer】26 二叉搜索树与双向链表
  6. 音视频开发(28)---流媒体并发量与宽带、码率计算详解
  7. EF中使用SQL语句或存储过程
  8. python中pandas有误_python-pandas to_sql方法给出日期列错误
  9. 如何判断web应用是否添加到主屏幕
  10. datagridview合并表头
  11. ipa 上传卡在鉴权_Application Loader上传app,一直卡在“正在通过 App Store 进行鉴定”...
  12. 算法刷题指南,来自GitHub 68.8k star的硬核算法教程
  13. 自动色彩均衡算法(ACE)原理及实现
  14. [2018.07.21 T3] Booom
  15. java开发的程序怎么用_java安装后怎么使用?第一次编写java程序
  16. ansys模型导入matlab,ANSYS导入MATLAB
  17. 网易云团队前端单元测试技术方案总结,测试人员必备知识
  18. python多级网址爬取_『采集超市』添加多级网址之手动填写链接地址规则
  19. Sphinx使用方法
  20. java获取明天的日期_使用java获取昨日的日期,今日的日期,明日的日期

热门文章

  1. 公交实时位置查询接口API
  2. 计算机网络技术 校园网规划,校园网规划与建设
  3. 深圳大学计算机考研2018,计算机考研:深圳大学2018年硕士研究生招生章程
  4. DLNU weekly(May 18,2013)解题报告
  5. AKABEiSOFT2经典作品推荐 車輪の国、向日葵の少女( 攻略、汉化、特典、PSP转换器)
  6. 实用新型专利申请书——一种基于蓝牙定位的地摊打卡装置
  7. uvm_agent,uvm_scoreboard,reference model
  8. 传智oracle,传智播客Oracle笔记
  9. 讲真,你与尖子生之间只有这本书的距离
  10. wpf html5 流畅性,iPad浏览器HTML5性能测试