1. 引言
本文主要是通过比较实用的挂起函数join
和await
来接触实践协程的挂起作用,同时本部分将会有较多的理解内容。
2. 等待协程执行完成
不多说,直接上代码!
某启动一个协程并将job对象保存下来:
viewBinding.launchBtn -> {
"Clicked launchBtn".let {
myLog(it)
}
job?.cancel()
job = scope.launch(Dispatchers.IO) {
"Coroutine IO runs (from launchBtn)".let {
myLog(it)
}
Thread.sleep(FIVE_SECONDS)
"Coroutine IO runs after thread sleep (from launchBtn)".let {
myLog(it)
}
}
}
然后另外一个地方,等待这个协程的执行结束,这里关键是join
函数!
viewBinding.joinBtn -> {
"Clicked joinBtn".let {
myLog(it)
}
scope.launch(Dispatchers.Main) {
"Coroutine Main runs (from joinBtn)".let {
myLog(it)
}
val jobNonNull = job ?: throw IllegalStateException("No job launched yet!")
jobNonNull.join()
"Coroutine Main runs after join() (from joinBtn)".let {
myLog(it)
}
}
}
这样的话,先点击launchBtn后在5秒内点击joinBtn,请问下面这两行log,输出的顺序会是?
"Coroutine IO runs after thread sleep (from launchBtn)"
"Coroutine Main runs after join() (from joinBtn)"
事实上,这两行的log的输出顺序,必然是先第一行再第二行!
这便是由于挂起函数join
的作用产生的效果!
挂起函数
join
的作用:挂起调用处所在的协程直到调用者协程执行完成。
3. 协程与线程等待完成函数的对照
协程中Job的join
函数与线程Thread的join
函数在功能设计上其实是类似的。
线程/协程对象的join函数调用后,将在调用处等待线程/协程对象执行完成后再继续往下执行。
好像比较笼统或不好理解?那么来个详细对比版吧:
在线程A执行过程中调用了线程B的join函数,那么线程A进入阻塞状态(BLOCKED),直到线程B执行完成后再转化为可执行状态(RUNNABLE),线程A在获得CPU时间片后再继续往下执行。
在协程C执行过程中调用了协程D的join函数,那么协程C进入挂起状态(SUSPENDED),直到协程D执行完成后再转换为恢复状态(RESUMED),协程C在获得调度器的调度后再继续往下执行。
这里尽量简洁了,如果还是看不懂?……那就……多看几遍?如果还是不懂?…………罢了罢了,不懂的话,建议先记下吧。
4. 关于挂起不得不提的点
说到协程的挂起,必要强调以下的核心内容:
1) 操作系统层面没有协程的存在;
2) 协程的挂起状态不对应任何的线程状态;
3) 协程处于挂起状态之时,不占用或阻塞任何线程;
4) 如果用的是runBlocking
方式启动协程,上面的第2和第3点将不再成立;
对于第2和第3点,这便是协程挂起的神奇之处!
挂起函数的调用,虽然在逻辑上是依次执行的,但是从操作系统执行字节码角度来看,挂起函数的执行过程却会是异步回调式的执行逻辑。
点到即止,这部分是协程挂起中非常核心的内容:CPS转换和状态机,有兴趣的可以拓展深入探究或学习。
这里是基础学习篇……
“哼,亏你还知道是基础学习篇,还放出这么多理解的内容不是想劝退?”
“对不起咯,实在没忍住,见谅见谅。”
个人觉得,说到协程的挂起,这些内容还是必须要提的,理解好不理解也罢,起码得有个印象,协程的挂起毕竟是非常核心且关键的内容。
5. 获得协程的执行结果返回
应该都知道,launch
方式启动的协程没有带有返回值,而async
方式启动的协程可以带有返回值。
可能有不知道的小伙伴?我不管,反正你现在知道了。
或许有小伙伴经不住会问,"啥玩意?launch函数不是明明有返回值Job吗?为啥说没有返回值呢?“
好吧,这部分其实是函数式编程设计的内容,我说的是协程带有返回值,说的是协程执行体(一般写法会是lambda表达式的函数体部分)的返回值,而不是launch函数的返回值。
如果这个没搞懂,建议先学习了解下Kotlin的函数类型、lambda表达式等函数式编程设计内容。
…………怎么感觉不大对?隐约间又说道别的内容了?好吧,没忍住。
赶紧上代码!
先是通过async
启动协程部分:
viewBinding.asyncBtn -> {
"Clicked asyncBtn".let {
myLog(it)
}
deferred?.cancel()
deferred = scope.async(Dispatchers.IO) {
val stringBuilder = StringBuilder()
"Coroutine IO runs (from asyncBtn)".let {
myLog(it)
}
Thread.sleep(FIVE_SECONDS)
"TeaC".apply {
"Coroutine IO runs after thread sleep: $this (from asyncBtn)".let {
myLog(it)
}
}
}
}
再是通过挂起函数await
获取所启动协程的返回值部分:
viewBinding.awaitBtn -> {
"Clicked awaitBtn".let {
myLog(it)
}
scope.launch(Dispatchers.Main) {
"Coroutine Main runs (from awaitBtn)".let {
myLog(it)
}
val deferredNonNull =
deferred ?: throw IllegalStateException("No deferred async yet!")
val ret = deferredNonNull.await()
"Coroutine Main runs after await(): $ret (from awaitBtn)".let {
myLog(it)
}
}
}
同样的,先点击asyncBtn然后5秒内点击awaitBtn,那么下面两行的日志输出将会始终保证顺序:
"Coroutine IO runs after thread sleep: $this (from asyncBtn)"
"Coroutine Main runs after await(): TeaC (from awaitBtn)"
与join
不同的是,await
是有返回值的,注意关键代码:
val ret = deferredNonNull.await()
上述代码,这里ret将会是async
启动的协程函数体里的返回值,当前实践代码中,类型是String,值为"TeaC"。
协程函数体的返回值?协程函数体里没看到有返回值的返回啊?好吧,这里搞清楚一个点,async
后的花括号部分其实是lambda表达式,而lambda表达式函数体部分的返回值会是最后一个表达式的返回值,可以有显式的return关键字方式,但是Kotlin开发文档中并不建议显式写出return这种方式……
好像有点不对?打住打住!这部分其实是Kotlin函数式编程内容,所以…………
回到上述代码,其实便是通过挂起函数await
,获得了async
所启动的协程函数体中的返回值。如目标协程还未结束时,将挂起等待最终结果的返回。
6. 两种协程启动方式的对比
两种协程启动方式,分别指的是launch和async启动协程的方式对比。
更具体地说,应该是(launch/Job/join)和(async/Deferred/await)这两个组合拳之间的对比。
- launch函数的返回值是Job,而async函数的返回值是Deferred<T>;
- launch启动的协程函数体的返回值必然是Unit,而async启动的协程函数体的返回值将是最后一个表达式的值;
- Job#join()和Deferred#await()均是挂起函数,都有挂起协程等待协程执行完成的作用,但是前者没有返回值(又或说返回值是Unit),后者有返回值,返回值将是async的协程函数体中的返回值;
事实上,两者对比上的差异远不止上述内容,比如在协程不同条件下的取消表现,关于join/await总结如下:
对于join
函数在各种场景下的总结:
1)协程B中调用了协程A的join函数后,协程B等待到协程A完成后才继续往下执行;
2)协程B在等待协程A完成的过程中,协程挂起,但协程B所执行在的线程并没有阻塞;
3)协程B在调用协程A的join函数前,协程A已经完成,则join函数被调用不会产生实际性效果且会继续下执行;
4)协程B在挂起等待协程A的过程中,如果协程A被取消,则协程B的挂起状态结束且继续正常往下执行;
5)协程B在挂起等待协程A的过程中,如果协程B被取消,则协程B在调用join函数之处会抛出CancellationException;
对于await
函数在各种场景下的总结:
1)协程B中调用了协程A的await函数后,协程B等待到协程A完成并返回结果后才继续往下执行;
2)协程B在等待协程A结果的过程中,协程挂起,但协程B所执行在的线程并没有阻塞;
3)协程B在调用协程A的await函数前,协程A已经完成并返回结果,则await函数直接返回协程A的执行结果且往下继续执行;
4)协程B在挂起等待协程A结果的过程中,如果协程A被取消,则协程B在调用协程A的await方法处抛出CancellationException;
5)协程B在挂起等待协程A结果的过程中,如果协程B被取消,则协程B在调用协程A的await方法处会抛出CancellationException;
不用担心异常CancellationException的抛出,在协程函数体和挂起函数执行中,异常CancellationException是用作协程取消协作点用的,前文的取消篇内容所用的ensureActive
函数的真正取消协作点也是抛出此种异常。
注:完整的实践代码中,也提供了协程取消的写法,根据已有的代码作进一步修改,可以实践验证上面的总结。
7. 样例工程代码
代码样例Demo,见Github:https://github.com/TeaCChen/CoroutineStudy
本文示例代码,如觉奇怪或啰嗦,其实为CancelStepTwoActivity.kt
中的代码摘取主要部分说明,在demo代码当中,为提升细节内容,有更加多的封装和输出内容。
本文的页面截图示例如下:
一学就会的协程使用——基础篇(六)初识挂起(本文)