一学就会的协程使用——基础篇(六)初遇挂起

1. 引言

本文主要是通过比较实用的挂起函数joinawait来接触实践协程的挂起作用,同时本部分将会有较多的理解内容。

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
  • 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代码当中,为提升细节内容,有更加多的封装和输出内容。

本文的页面截图示例如下:

image-6-1.png

一学就会的协程使用——基础篇

一学就会的协程使用——基础篇(一)协程启动

一学就会的协程使用——基础篇(二)线程切换

一学就会的协程使用——基础篇(三)初遇协程取消

一学就会的协程使用——基础篇(四)协程作用域

一学就会的协程使用——基础篇(五)再遇协程取消

一学就会的协程使用——基础篇(六)初识挂起(本文)

一学就会的协程使用——基础篇(七)初识结构化

一学就会的协程使用——基础篇(八)初识协程异常

一学就会的协程使用——基础篇(九)异常与supervisor

你可能感兴趣的:(一学就会的协程使用——基础篇(六)初遇挂起)