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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容