android kotlin 协程 理解挂起,恢复以及job
Posted 史大拿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android kotlin 协程 理解挂起,恢复以及job相关的知识,希望对你有一定的参考价值。
android kotlin 协程(三) 理解挂起,恢复以及job
前言: 通过上两篇的基础入门,相信大家对协程api已经有了一个基本的影响,本篇开始尝试理解挂起于恢复.
本篇不涉及源码, 通过很多应用案例,来理解挂起于恢复!
协程执行流程理解
还是老套路,先来看原始效果
参考图1
在这里我们知道runBlocking会阻塞主线程来等待子协程的执行, 我们通过了kotlin提供的API measureTimeMillis()
来计算出runblocking的执行时间
**kotlin协程本质就是一个线程框架,**我们在使用线程的时候, 通过调用Thread#start() 来通知JVM我们需要执行一个任务
然后JVM去调度执行
协程也是类似的,在协程中也有调度的概念
在第一篇中我们提到过, 只要是suspend函数,一定是异步的,除非自己想不开调用Thread.sleep,这也是学习协程我认为最重要的一点
首先我们要知道launch 中会做那些事情, 通过CoroutineScope#launch 创建一个协程的时候, 首先会进入到调度前准备阶段,也就是开启一个协程
开启一个协程的时候并不是执行阶段,只会将这个协程标记为活跃状态,此时这种状态是不会执行协程体中的代码
先有调度, 在有执行
参考图2
- Job#isActivte 协程是否活跃
- Job#isCancelled 协程是否被取消
- Job#isCompleated 协程是否执行完成协程体
因为协程需要调度, 并且这段代码是运行到main线程的,所以这段代码默认会执行调度前的代码,然后才是执行**调度后(执行阶段)**的代码,
这段代码是运行在Main线程中的,如果我们将他放入IO线程,看看会有什么变化
参考图3
可以看出,Main线程 和 IO线程打印出来的执行顺序是不一样的,
这一点也好理解, 因为子协程会跟随父协程的线程,所以子协程中打印就会有时快有时慢
这取决与你CPU的运行速度
但是有一点,即使是IO线程,也会有调度的说法
这里有细心的朋友也可能会看到:
按照上面的说法,在这里代码3
先执行,然后在执行的代码5
,那么为什么此时Job还是活跃,未完成状态呢?
这是因为他虽然执行完了代码块中的代码,但是还是挂起状态,直到恢复之后才是完成状态!
有了这个前提,接下来步入到本篇的核心, 理解 挂起于恢复
我没有学习协程之前和学一段时间协程后看到挂起与恢复这两个字也很陌生
因为我们知道,挂起是通过suspend
关键字 来标记的,但是恢复,怎么恢复? 谁来恢复?
这里先剧透一下后面的内容,不深究,先有个概念就好
我们都知道kotlin是可以反编译为java代码的,但是java中并没有suspend关键字,
那么我们要在java中调用kotlin中的suspend函数,就会要传入一个Continuation的东西, 其本质就是一个回调
简单的说就是 : suspend关键字 就是 Continuation
如果你看过Continuation这个类就知道, 恢复其实指的就是 Continuation#resumeWith
因为我们并没有Continuation 对象,我们只有suspend关键字,所以恢复的代码全部都是kotlin帮我们完成的
恢复其本质就是kotlin帮我们调用 Continuation#resumeWith
反编译看看:
好了,这里就不展开讲了,后面在聊这些,直到这行代码,你只要清楚,恢复的工作,是不需要我们的,是kotlin帮我们完成的
就像是开自动挡车一样,不需要踩离合,并且也没有离合
接下来看一些案例,来更好的理解挂起于恢复, 我们都知道 delay() 是挂起函数,那么我们通过比较 Thread.sleep 与 delay 的区别来理解 挂起于恢复
案例1
sleep | delay |
---|---|
sleep()代码应该比较好理解, 就是正常代码阻塞,我不执行完,你不准执行
就好像是地铁安检一样, 如果前面的那个人身上有危险物品,那么安检员就一直等待着,直到排除这个人没问题之后再放行
delay()则不同,delay是一个挂起函数, 当他挂起的时候,会恢复现在已经挂起的函数,也就是会执行 代码3
中的代码
这也就是我为什么说suspend函数中的代码永远是异步的
delay()中的代码很简单,也很重要,对理解挂起于恢复有很大的帮助,建议反复琢磨!
案例2
再来看一个suspend不会阻塞的代码
sleep | delay |
---|---|
阻塞:用时3s | 非阻塞:用时2s |
sleep就不用过多介绍了,还是按顺序执行,并不会有任何变化
delay(): 这里需要注意的是 代码4
比代码3
执行的更快! 并且总耗时为2s, 这就证明了两个协程之前并不会有关联,他们都是异步执行的,虽然在同一个Main线程内!
说白点就是: 在Main线程内异步执行代码!
如果是线程,要写这样的功能该怎么写呢?
先thread 然后执行完异步任务之后在通过handler切换到main线程
案例3
接下来,就不看结果了, 你能正确打印出他的执行流程嘛?
fun main()
println("main start") // 1
runBlocking<Unit>
println("runBlocking start") // 2
delay(2000)
println("runBlocking end") // 3
println("main 执行中..") // 4
Thread.sleep(2100)
println("main end") // 5
| |
| |
| 占位符 |
| |
| |
这段代码比较简单,我们从第一篇就开始说, runBlocking 会阻塞主线程来等待自己子协程执行,这里没有子协程,只有一个delay,所以会等待delay执行完再往下执行
所以这里的顺序为
1,2,3,4,5
案例4
fun main() = runBlocking<Unit>
println("main start") // 1
val job = launch // 协程1
println("launch 1 start") // 2
delay(1000L) // TODO 延时1
println("launch 1 end") // 3
println("main mid") // 4
val job2 = launch // 协程2
println("launch 2 start") //5
delay(1000L) // TODO 延时2
println("launch 2 end") //6
delay(1500) // TODO 延时3
println("main end") // 7
| |
| |
| 占位符 |
| |
| |
这里难度稍微升级了,在这块代码中,一定是先执行调度前的代码
也就是 1, 4 和 延时3
因为延时3
会恢复正在挂起的函数执行
所以会执行 协程1
和 协程2
中的代码
那么首先肯定先执行 2
敲黑板了,协程始终是异步的,所以这里执行完 2,就会执行 5
然后同时等待1秒
那么正确结果为:
1,4,2,5,3,6,7
案例5
| |
| |
| 占位符 |
| |
| |
这里理解的是Job状态的掌握, 你可以打印一下Job的三种状态[isActive,isCancelled,isCompleted]
首先执行调度前的任务执行 1,4
这始终都是不变的
但是执行到 **job.join()**的时候,会等待协程1执行完,那么就会执行协程1
中的代码
现在的顺序为1,4,2,3
此时,job的状态为 isActive = false, isCompleted = true
说明这个job已经执行完了,这个job生命已经结束了
那么将一个没有生命的job给到一个新的协程中,运行起来可想而知,这个协程也是没有生命的
所以协程2中的代码不会执行
最终正确结果为:
1,4,2,3,7
案例6
| |
| |
| 占位符 |
| |
| |
在这段代码中,重点就是协程2
使用了 父协程的coroutineContext 与 自己的DIspatchers.IO
由案例5知道,
在join之前的代码执行结果都一样
1,4,2,3
然后最开始执行协程2
,在这段代码中,因为我们是用了自己的Dispatchers.IO,所以开启的了一个IO子协程
最重要的是coroutineContext[Job] 是什么状态
因为这个Job是在coroutineScope内部使用的,很明显,
Job的状态 isActive = true, isCompleted = false, isCancelled = false
然后会通过阻塞2
阻塞线程2s
这里的关键点:
- coroutineContext[Job] 存活 可以执行
协程2
,中协程体的代码 - Dispatchers.IO 不会阻塞线程
所以最终结果为:
1,4,2,3,5,6,7
这个例子稍微有点复杂,正常开发中碰到这种代码建议重构!
案例7
| |
| |
| 占位符 |
| |
| |
默认执行调度前的代码我就不过多说了,首先会执行1,7
然后通过阻塞1
阻塞500ms,
此时协程1
是异步的,所以会执行 2
这里有一个关键点:
一定要分清楚Job 和 coroutineContext
协程1
中传入的Dispatchers.IO 是 coroutineContext, 返回的Job是异步并没有任何关系!!
协程2
使用了协程1
的job并不会有任何作用, 因为协程2
不是异步的,所以他会等待阻塞1
执行完毕后在执行
当阻塞1
执行完的时候,然后立即Job#cancel()了
所以协程1,协程2,协程3都会被cancel掉
最终的执行结果为:
1,7,2,9
案例8
// TODO ======== 案例8 ===================
fun main()
val useTime = measureTimeMillis
runBlocking<Unit>
println("main start") // 1
val job = GlobalScope.launch // TODO 全局协程
println("launch 1 start") // 2
delay(1000L) // 延迟1
println("launch 1 end") // 3
println("main mid") // 4
launch(context = job) // TODO 子协程
println("launch 2 start") // 5
delay(2000L) // 延迟2
println("launch 2 end") // 6
println("main end") // 7
println("使用时间: $useTime")
| |
| |
| 占位符 |
| |
| |
这段代码有点刁钻,刚写出来的时候,我自己都没看明白是怎么回事,然后细细品味了一番才悟了!
首先执行调度前的代码是没问题的:
1,4
但此时并不会执行7, 因为在执行子协程的时候使用到了job,既然使用到了另一个job,那么就必须吧 全局协程
执行完才有job
所以这里会先执行2, 然后 遇到 延迟1
所以目前的打印顺序为
1,4,2
然后子协程
被创建, 并不会执行协程体中的代码,因为当前调度前的代码还没有走完
所以会先执行调度前的代码
1,4,2,7
最终你认为会执行全局作用域
和 子协程
体中的代码?
那就大错特错了!!!
还记得runBlocking是干什么用的嘛?
runBlocking 只会等待自己子协程执行
GlobalScope#launch 很显然不是runBlocking 的子协程
那么,子协程
肯定是runBlocking的子协程吧, 是的,没问题
可是这里的job,是使用的GlobalScope的job
job的作用是什么呢?
管理协程的生命周期等
runBlocking 都不会等待GlobalScope作用域,更不会等待使用GlobalScope job的作用域
所以这里的最终结果为:
1,4,2,7,5
下一篇预告: 协程之间的通信
- channel
- produce
- actor
- broadcastChannel
- select
原创不易,您的点赞就是对我最大的支持!
以上是关于android kotlin 协程 理解挂起,恢复以及job的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )
Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )
Kotlin 协程协程的挂起和恢复 ② ( 协程挂起 和 线程阻塞 对比 )