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

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

作者: TeaCChen | 来源:发表于2021-09-10 22:07 被阅读0次

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

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

    image-6-1.png

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

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

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

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

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

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

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

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

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

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/vsnswltx.html