破解 Kotlin 協(xié)程(4) - 異常處理篇

關(guān)鍵詞:Kotlin 協(xié)程 異常處理

異步代碼的異常處理通常都比較讓人頭疼,而協(xié)程則再一次展現(xiàn)了它的威力柔滔。

1. 引子

我們?cè)谇懊嬉黄恼庐?dāng)中提到了這樣一個(gè)例子:

typealias Callback = (User) -> Unit

fun getUser(callback: Callback){
    ...
}

我們通常會(huì)定義這樣的回調(diào)接口來(lái)實(shí)現(xiàn)異步數(shù)據(jù)的請(qǐng)求眶诈,我們可以很方便的將它轉(zhuǎn)換成協(xié)程的接口:

suspend fun getUserCoroutine() = suspendCoroutine<User> {
    continuation ->
    getUser {
        continuation.resume(it)
    }
}

并最終交給按鈕點(diǎn)擊事件或者其他事件去觸發(fā)這個(gè)異步請(qǐng)求:

getUserBtn.setOnClickListener {
    GlobalScope.launch(Dispatchers.Main) {
        userNameView.text = getUserCoroutine().name
    }
}

那么問(wèn)題來(lái)了耳胎,既然是請(qǐng)求,總會(huì)有失敗的情形闲勺,而我們這里并沒(méi)有對(duì)錯(cuò)誤的處理砚亭,接下來(lái)我們就完善這個(gè)例子灯变。

2. 添加異常處理邏輯

首先我們加上異常回調(diào)接口函數(shù):

interface Callback<T> {
    fun onSuccess(value: T)

    fun onError(t: Throwable)
}

接下來(lái)我們?cè)诟脑煲幌挛覀兊?getUserCoroutine

suspend fun getUserCoroutine() = suspendCoroutine<User> { continuation ->
    getUser(object : Callback<User> {
        override fun onSuccess(value: User) {
            continuation.resume(value)
        }

        override fun onError(t: Throwable) {
            continuation.resumeWithException(t)
        }
    })
}

大家可以看到捅膘,我們似乎就是完全把 Callback 轉(zhuǎn)換成了一個(gè) Continuation添祸,在調(diào)用的時(shí)候我們只需要:

GlobalScope.launch(Dispatchers.Main) {
    try {
        userNameView.text = getUserCoroutine().name
    } catch (e: Exception) {
        userNameView.text = "Get User Error: $e"
    }
}

是的,你沒(méi)看錯(cuò)寻仗,一個(gè)異步的請(qǐng)求異常刃泌,我們只需要在我們的代碼中捕獲就可以了,這樣做的好處就是愧沟,請(qǐng)求的全流程異常都可以在一個(gè) try ... catch ... 當(dāng)中捕獲蔬咬,那么我們可以說(shuō)真正做到了把異步代碼變成了同步的寫(xiě)法鲤遥。

如果你一直在用 RxJava 處理這樣的邏輯沐寺,那么你的請(qǐng)求接口可能是這樣的:

fun getUserObservable(): Single<User> {
    return Single.create<User> { emitter ->
        getUser(object : Callback<User> {
            override fun onSuccess(value: User) {
                emitter.onSuccess(value)
            }

            override fun onError(t: Throwable) {
                emitter.onError(t)
            }
        })
    }
}

調(diào)用時(shí)大概是這樣的:

getUserObservable()
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe ({ user ->
            userNameView.text = user.name
        }, {
            userNameView.text = "Get User Error: $it"
        })

其實(shí)你很容易就能發(fā)現(xiàn)在這里 RxJava 做的事兒跟協(xié)程的目的是一樣的,只不過(guò)協(xié)程用了一種更自然的方式盖奈。

也許你已經(jīng)對(duì) RxJava 很熟悉并且感到很自然混坞,但相比之下,RxJava 的代碼比協(xié)程的復(fù)雜度更高钢坦,更讓人費(fèi)解究孕,這一點(diǎn)我們后面的文章中也會(huì)持續(xù)用例子來(lái)說(shuō)明這一點(diǎn)。

3. 全局異常處理

線程也好爹凹、RxJava 也好厨诸,都有全局處理異常的方式,例如:

fun main() {
    Thread.setDefaultUncaughtExceptionHandler {t: Thread, e: Throwable ->
        //handle exception here
        println("Thread '${t.name}' throws an exception with message '${e.message}'")
    }

    throw ArithmeticException("Hey!")
}

我們可以為線程設(shè)置全局的異常捕獲禾酱,當(dāng)然也可以為 RxJava 來(lái)設(shè)置全局異常捕獲:

RxJavaPlugins.setErrorHandler(e -> {
        //handle exception here
        println("Throws an exception with message '${e.message}'")
});

協(xié)程顯然也可以做到這一點(diǎn)微酬。類(lèi)似于通過(guò) Thread.setUncaughtExceptionHandler 為線程設(shè)置一個(gè)異常捕獲器绘趋,我們也可以為每一個(gè)協(xié)程單獨(dú)設(shè)置 CoroutineExceptionHandler,這樣協(xié)程內(nèi)部未捕獲的異常就可以通過(guò)它來(lái)捕獲:

private suspend fun main(){
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        log("Throws an exception with message: ${throwable.message}")
    }

    log(1)
    GlobalScope.launch(exceptionHandler) {
        throw ArithmeticException("Hey!")
    }.join()
    log(2)
}

運(yùn)行結(jié)果:

19:06:35:087 [main] 1
19:06:35:208 [DefaultDispatcher-worker-1 @coroutine#1] Throws an exception with message: Hey!
19:06:35:211 [DefaultDispatcher-worker-1 @coroutine#1] 2

CoroutineExceptionHandler 竟然也是一個(gè)上下文颗管,協(xié)程的這個(gè)上下文可真是靈魂一般的存在陷遮,這倒是一點(diǎn)兒也不讓人感到意外。

當(dāng)然垦江,這并不算是一個(gè)全局的異常捕獲帽馋,因?yàn)樗荒懿东@對(duì)應(yīng)協(xié)程內(nèi)未捕獲的異常,如果你想做到真正的全局捕獲比吭,在 Jvm 上我們可以自己定義一個(gè)捕獲類(lèi)實(shí)現(xiàn):

class GlobalCoroutineExceptionHandler: CoroutineExceptionHandler {
    override val key: CoroutineContext.Key<*> = CoroutineExceptionHandler

    override fun handleException(context: CoroutineContext, exception: Throwable) {
        println("Coroutine exception: $exception")
    }
}

然后在 classpath 中創(chuàng)建 META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler绽族,文件名實(shí)際上就是 CoroutineExceptionHandler 的全類(lèi)名,文件內(nèi)容就寫(xiě)我們的實(shí)現(xiàn)類(lèi)的全類(lèi)名:

com.bennyhuo.coroutines.sample2.exceptions.GlobalCoroutineExceptionHandler

這樣協(xié)程中沒(méi)有被捕獲的異常就會(huì)最終交給它處理衩藤。

Jvm 上全局 CoroutineExceptionHandler 的配置项秉,本質(zhì)上是對(duì) ServiceLoader 的應(yīng)用,之前我們?cè)谥v Dispatchers.Main 的時(shí)候提到過(guò)慷彤,Jvm 上它的實(shí)現(xiàn)也是通過(guò) ServiceLoader 來(lái)加載的娄蔼。

需要明確的一點(diǎn)是,通過(guò) async 啟動(dòng)的協(xié)程出現(xiàn)未捕獲的異常時(shí)會(huì)忽略 CoroutineExceptionHandler底哗,這與 launch 的設(shè)計(jì)思路是不同的岁诉。

4. 異常傳播

異常傳播還涉及到協(xié)程作用域的概念,例如我們啟動(dòng)協(xié)程的時(shí)候一直都是用的 GlobalScope跋选,意味著這是一個(gè)獨(dú)立的頂級(jí)協(xié)程作用域涕癣,此外還有 coroutineScope { ... } 以及 supervisorScope { ... }

  • 通過(guò) GlobeScope 啟動(dòng)的協(xié)程單獨(dú)啟動(dòng)一個(gè)協(xié)程作用域前标,內(nèi)部的子協(xié)程遵從默認(rèn)的作用域規(guī)則坠韩。通過(guò) GlobeScope 啟動(dòng)的協(xié)程“自成一派”。
  • coroutineScope 是繼承外部 Job 的上下文創(chuàng)建作用域炼列,在其內(nèi)部的取消操作是雙向傳播的只搁,子協(xié)程未捕獲的異常也會(huì)向上傳遞給父協(xié)程。它更適合一系列對(duì)等的協(xié)程并發(fā)的完成一項(xiàng)工作俭尖,任何一個(gè)子協(xié)程異常退出氢惋,那么整體都將退出,簡(jiǎn)單來(lái)說(shuō)就是”一損俱損“稽犁。這也是協(xié)程內(nèi)部再啟動(dòng)子協(xié)程的默認(rèn)作用域焰望。
  • supervisorScope 同樣繼承外部作用域的上下文,但其內(nèi)部的取消操作是單向傳播的已亥,父協(xié)程向子協(xié)程傳播熊赖,反過(guò)來(lái)則不然,這意味著子協(xié)程出了異常并不會(huì)影響父協(xié)程以及其他兄弟協(xié)程虑椎。它更適合一些獨(dú)立不相干的任務(wù)震鹉,任何一個(gè)任務(wù)出問(wèn)題的妖,并不會(huì)影響其他任務(wù)的工作,簡(jiǎn)單來(lái)說(shuō)就是”自作自受“足陨,例如 UI嫂粟,我點(diǎn)擊一個(gè)按鈕出了異常,其實(shí)并不會(huì)影響手機(jī)狀態(tài)欄的刷新墨缘。需要注意的是星虹,supervisorScope 內(nèi)部啟動(dòng)的子協(xié)程內(nèi)部再啟動(dòng)子協(xié)程,如無(wú)明確指出镊讼,則遵守默認(rèn)作用域規(guī)則宽涌,也即 supervisorScope 只作用域其直接子協(xié)程。

這么說(shuō)還是比較抽象蝶棋,因此我們拿一些例子來(lái)分析一下:

suspend fun main() {
    log(1)
    try {
        coroutineScope { //①
            log(2)
            launch { // ②
                log(3)
                launch { // ③ 
                    log(4)
                    delay(100)
                    throw ArithmeticException("Hey!!")
                }
                log(5)
            }
            log(6)
            val job = launch { // ④
                log(7)
                delay(1000)
            }
            try {
                log(8)
                 job.join()
                log("9")
            } catch (e: Exception) {
                log("10. $e")
            }
        }
        log(11)
    } catch (e: Exception) {
        log("12. $e")
    }
    log(13)
}

這例子稍微有點(diǎn)兒復(fù)雜卸亮,但也不難理解,我們?cè)谝粋€(gè) coroutineScope 當(dāng)中啟動(dòng)了兩個(gè)協(xié)程 ②④玩裙,在 ② 當(dāng)中啟動(dòng)了一個(gè)子協(xié)程 ③兼贸,作用域直接創(chuàng)建的協(xié)程記為①。那么 ③ 當(dāng)中拋異常會(huì)發(fā)生什么呢吃溅?我們先來(lái)看下輸出:

11:37:36:208 [main] 1
11:37:36:255 [main] 2
11:37:36:325 [DefaultDispatcher-worker-1] 3
11:37:36:325 [DefaultDispatcher-worker-1] 5
11:37:36:326 [DefaultDispatcher-worker-3] 4
11:37:36:331 [main] 6
11:37:36:336 [DefaultDispatcher-worker-1] 7
11:37:36:336 [main] 8
11:37:36:441 [DefaultDispatcher-worker-1] 10. kotlinx.coroutines.JobCancellationException: ScopeCoroutine is cancelling; job=ScopeCoroutine{Cancelling}@2bc92d2f
11:37:36:445 [DefaultDispatcher-worker-1] 12. java.lang.ArithmeticException: Hey!!
11:37:36:445 [DefaultDispatcher-worker-1] 13

注意兩個(gè)位置溶诞,一個(gè)是 10,我們調(diào)用 join决侈,收到了一個(gè)取消異常螺垢,在協(xié)程當(dāng)中支持取消的操作的suspend方法在取消時(shí)會(huì)拋出一個(gè) CancellationException,這類(lèi)似于線程中對(duì) InterruptException 的響應(yīng)赖歌,遇到這種情況表示 join 調(diào)用所在的協(xié)程已經(jīng)被取消了枉圃,那么這個(gè)取消究竟是怎么回事呢?

原來(lái)協(xié)程 ③ 拋出了未捕獲的異常庐冯,進(jìn)入了異常完成的狀態(tài)孽亲,它與父協(xié)程 ② 之間遵循默認(rèn)的作用域規(guī)則,因此 ③ 會(huì)通知它的父協(xié)程也就是 ② 取消肄扎,② 根據(jù)作用域規(guī)則通知父協(xié)程 ① 也就是整個(gè)作用域取消墨林,這是一個(gè)自下而上的一次傳播赁酝,這樣身處 ① 當(dāng)中的 job.join 調(diào)用就會(huì)拋異常犯祠,也就是 10 處的結(jié)果了。如果不是很理解這個(gè)操作酌呆,想一下我們說(shuō)到的衡载,coroutineScope 內(nèi)部啟動(dòng)的協(xié)程就是“一損俱損”。實(shí)際上由于父協(xié)程 ① 被取消隙袁,協(xié)程④ 也不能幸免痰娱,如果大家有興趣的話(huà)弃榨,也可以對(duì) ④ 當(dāng)中的 delay進(jìn)行捕獲,一樣會(huì)收獲一枚取消異常梨睁。

還有一個(gè)位置就是 12鲸睛,這個(gè)是我們對(duì) coroutineScope 整體的一個(gè)捕獲,如果 coroutineScope 內(nèi)部以為異常而結(jié)束坡贺,那么我們是可以對(duì)它直接 try ... catch ... 來(lái)捕獲這個(gè)異常的官辈,這再一次表明協(xié)程把異步的異常處理到同步代碼邏輯當(dāng)中。

那么如果我們把 coroutineScope 換成 supervisorScope遍坟,其他不變拳亿,運(yùn)行結(jié)果會(huì)是怎樣呢?

11:52:48:632 [main] 1
11:52:48:694 [main] 2
11:52:48:875 [main] 6
11:52:48:892 [DefaultDispatcher-worker-1 @coroutine#1] 3
11:52:48:895 [DefaultDispatcher-worker-1 @coroutine#1] 5
11:52:48:900 [DefaultDispatcher-worker-3 @coroutine#3] 4
11:52:48:905 [DefaultDispatcher-worker-2 @coroutine#2] 7
11:52:48:907 [main] 8
Exception in thread "DefaultDispatcher-worker-3 @coroutine#3" java.lang.ArithmeticException: Hey!!
    at com.bennyhuo.coroutines.sample2.exceptions.ScopesKt$main$2$1$1.invokeSuspend(Scopes.kt:17)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
11:52:49:915 [DefaultDispatcher-worker-3 @coroutine#2] 9
11:52:49:915 [DefaultDispatcher-worker-3 @coroutine#2] 11
11:52:49:915 [DefaultDispatcher-worker-3 @coroutine#2] 13

我們可以看到愿伴,1-8 的輸出其實(shí)沒(méi)有本質(zhì)區(qū)別肺魁,順序上的差異是線程調(diào)度的前后造成的,并不會(huì)影響協(xié)程的語(yǔ)義隔节。差別主要在于 9 與 10鹅经、11與12的區(qū)別,如果把 scope 換成 supervisorScope怎诫,我們發(fā)現(xiàn) ③ 的異常并沒(méi)有影響作用域以及作用域內(nèi)的其他子協(xié)程的執(zhí)行瞬雹,也就是我們所說(shuō)的“自作自受”。

這個(gè)例子其實(shí)我們?cè)偕宰鲆恍└膭?dòng)刽虹,為 ② 和 ③ 增加一個(gè) CoroutineExceptionHandler酗捌,就可以證明我們前面提到的另外一個(gè)結(jié)論:

首先我們定義一個(gè) CoroutineExceptionHandler,我們通過(guò)上下文獲取一下異常對(duì)應(yīng)的協(xié)程的名字:

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    log("${coroutineContext[CoroutineName]} $throwable")
}

接著涌哲,基于前面的例子我們?yōu)?② 和 ③ 添加 CoroutineExceptionHandler 和名字:

...
supervisorScope { //①
    log(2)
    launch(exceptionHandler + CoroutineName("②")) { // ②
        log(3)
        launch(exceptionHandler + CoroutineName("③")) { // ③
            log(4)
...

再運(yùn)行這段程序胖缤,結(jié)果就比較有意思了:

...
07:30:11:519 [DefaultDispatcher-worker-1] CoroutineName(②) java.lang.ArithmeticException: Hey!!
...

我們發(fā)現(xiàn)觸發(fā)的 CoroutineExceptionHandler 竟然是協(xié)程 ② 的,意外嗎阀圾?不意外哪廓,因?yàn)槲覀兦懊嬉呀?jīng)提到,對(duì)于 supervisorScope 的子協(xié)程 (例如 ②)的子協(xié)程(例如 ③)初烘,如果沒(méi)有明確指出涡真,它是遵循默認(rèn)的作用于規(guī)則的,也就是 coroutineScope 的規(guī)則了肾筐,出現(xiàn)未捕獲的異常會(huì)嘗試傳遞給父協(xié)程并嘗試取消父協(xié)程哆料。

究竟使用什么 Scope,大家自己根據(jù)實(shí)際情況來(lái)確定吗铐,我給出一些建議:

  • 對(duì)于沒(méi)有協(xié)程作用域东亦,但需要啟動(dòng)協(xié)程的時(shí)候,適合用 GlobalScope
  • 對(duì)于已經(jīng)有協(xié)程作用域的情況(例如通過(guò) GlobalScope 啟動(dòng)的協(xié)程體內(nèi))唬渗,直接用協(xié)程啟動(dòng)器啟動(dòng)
  • 對(duì)于明確要求子協(xié)程之間相互獨(dú)立不干擾時(shí)典阵,使用 supervisorScope
  • 對(duì)于通過(guò)標(biāo)準(zhǔn)庫(kù) API 創(chuàng)建的協(xié)程奋渔,這樣的協(xié)程比較底層,沒(méi)有 Job壮啊、作用域等概念的支撐嫉鲸,例如我們前面提到過(guò) suspend main 就是這種情況,對(duì)于這種情況優(yōu)先考慮通過(guò) coroutineScope 創(chuàng)建作用域歹啼;更進(jìn)一步充坑,大家盡量不要直接使用標(biāo)準(zhǔn)庫(kù) API,除非你對(duì) Kotlin 的協(xié)程機(jī)制非常熟悉染突。

當(dāng)然捻爷,對(duì)于可能出異常的情況,請(qǐng)大家盡量做好異常處理份企,不要將問(wèn)題復(fù)雜化也榄。

5. join 和 await

前面我們舉例子一直用的是 launch,啟動(dòng)協(xié)程其實(shí)常用的還有 async司志、actorproduce甜紫,其中 actorlaunch 的行為類(lèi)似,在未捕獲的異常出現(xiàn)以后骂远,會(huì)被當(dāng)做為處理的異常拋出囚霸,就像前面的例子那樣。而 asyncproduce 則主要是用來(lái)輸出結(jié)果的激才,他們內(nèi)部的異常只在外部消費(fèi)他們的結(jié)果時(shí)拋出拓型。這兩組協(xié)程的啟動(dòng)器,你也可以認(rèn)為分別是“消費(fèi)者”和“生產(chǎn)者”瘸恼,消費(fèi)者異常立即拋出劣挫,生產(chǎn)者只有結(jié)果消費(fèi)時(shí)拋出異常。

actorproduce 這兩個(gè) API 目前處于比較微妙的境地东帅,可能會(huì)被廢棄或者后續(xù)提供替代方案压固,不建議大家使用,我們?cè)谶@里就不展開(kāi)細(xì)講了靠闭。

那么消費(fèi)結(jié)果指的是什么呢帐我?對(duì)于 async 來(lái)講,就是 await愧膀,例如:

suspend fun main() {
    val deferred = GlobalScope.async<Int> { 
        throw ArithmeticException()
    }
    try {
        val value = deferred.await()
        log("1. $value")
    } catch (e: Exception) {
        log("2. $e")
    }
}

這個(gè)從邏輯上很好理解拦键,我們調(diào)用 await 時(shí),期望 deferred 能夠給我們提供一個(gè)合適的結(jié)果扇调,但它因?yàn)槌霎惓?蠊荆瑳](méi)有辦法做到這一點(diǎn),因此只好給我們丟出一個(gè)異常了狼钮。

13:25:14:693 [main] 2. java.lang.ArithmeticException

我們自己實(shí)現(xiàn)的 getUserCoroutine 也屬于類(lèi)似的情況碳柱,在獲取結(jié)果時(shí),如果請(qǐng)求出了異常熬芜,我們就只能拿到一個(gè)異常莲镣,而不是正常的結(jié)果。相比之下涎拉,join 就有趣的多了瑞侮,它只關(guān)注是否執(zhí)行完,至于是因?yàn)槭裁赐瓿晒呐。魂P(guān)心半火,因此如果我們?cè)谶@里替換成 join

suspend fun main() {
    val deferred = GlobalScope.async<Int> {
        throw ArithmeticException()
    }
    try {
        deferred.join()
        log(1)
    } catch (e: Exception) {
        log("2. $e")
    }
}

我們就會(huì)發(fā)現(xiàn),異常被吞掉了季俩!

13:26:15:034 [main] 1

如果例子當(dāng)中我們用 launch 替換 async钮糖,join 處仍然不會(huì)有任何異常拋出,還是那句話(huà)酌住,它只關(guān)心有沒(méi)有完成店归,至于怎么完成的它不關(guān)心。不同之處在于酪我, launch 中未捕獲的異常與 async 的處理方式不同消痛,launch 會(huì)直接拋出給父協(xié)程,如果沒(méi)有父協(xié)程(頂級(jí)作用域中)或者處于 supervisorScope 中父協(xié)程不響應(yīng)都哭,那么就交給上下文中指定的 CoroutineExceptionHandler處理秩伞,如果沒(méi)有指定,那傳給全局的 CoroutineExceptionHandler 等等欺矫,而 async 則要等 await 來(lái)消費(fèi)稠歉。

不管是哪個(gè)啟動(dòng)器,在應(yīng)用了作用域之后汇陆,都會(huì)按照作用域的語(yǔ)義進(jìn)行異常擴(kuò)散怒炸,進(jìn)而觸發(fā)相應(yīng)的取消操作,對(duì)于 async 來(lái)說(shuō)就算不調(diào)用 await 來(lái)獲取這個(gè)異常毡代,它也會(huì)在 coroutineScope 當(dāng)中觸發(fā)父協(xié)程的取消邏輯阅羹,這一點(diǎn)請(qǐng)大家注意。

6. 小結(jié)

這一篇我們講了協(xié)程的異常處理教寂。這一塊兒稍微顯得有點(diǎn)兒復(fù)雜幔荒,但仔細(xì)理一下主要有三條線:

  1. 協(xié)程內(nèi)部異常處理流程:launch 會(huì)在內(nèi)部出現(xiàn)未捕獲的異常時(shí)嘗試觸發(fā)對(duì)父協(xié)程的取消,能否取消要看作用域的定義榴啸,如果取消成功滓彰,那么異常傳遞給父協(xié)程,否則傳遞給啟動(dòng)時(shí)上下文中配置的 CoroutineExceptionHandler 中,如果沒(méi)有配置看尼,會(huì)查找全局(JVM上)的 CoroutineExceptionHandler 進(jìn)行處理递鹉,如果仍然沒(méi)有,那么就將異常交給當(dāng)前線程的 UncaughtExceptionHandler 處理藏斩;而 async 則在未捕獲的異常出現(xiàn)時(shí)同樣會(huì)嘗試取消父協(xié)程躏结,但不管是否能夠取消成功都不會(huì)后其他后續(xù)的異常處理,直到用戶(hù)主動(dòng)調(diào)用 await 時(shí)將異常拋出狰域。
  2. 異常在作用域內(nèi)的傳播:當(dāng)協(xié)程出現(xiàn)異常時(shí)媳拴,會(huì)根據(jù)當(dāng)前作用域觸發(fā)異常傳遞,GlobalScope 會(huì)創(chuàng)建一個(gè)獨(dú)立的作用域兆览,所謂“自成一派”屈溉,而 在 coroutineScope 當(dāng)中協(xié)程異常會(huì)觸發(fā)父協(xié)程的取消,進(jìn)而將整個(gè)協(xié)程作用域取消掉抬探,如果對(duì) coroutineScope 整體進(jìn)行捕獲子巾,也可以捕獲到該異常,所謂“一損俱損”驶睦;如果是 supervisorScope砰左,那么子協(xié)程的異常不會(huì)向上傳遞,所謂“自作自受”场航。
  3. join 和 await 的不同:join 只關(guān)心協(xié)程是否執(zhí)行完缠导,await 則關(guān)心運(yùn)行的結(jié)果,因此 join 在協(xié)程出現(xiàn)異常時(shí)也不會(huì)拋出該異常溉痢,而 await 則會(huì)僻造;考慮到作用域的問(wèn)題,如果協(xié)程拋異常孩饼,可能會(huì)導(dǎo)致父協(xié)程的取消髓削,因此調(diào)用 join 時(shí)盡管不會(huì)對(duì)協(xié)程本身的異常進(jìn)行拋出,但如果 join 調(diào)用所在的協(xié)程被取消镀娶,那么它會(huì)拋出取消異常立膛,這一點(diǎn)需要留意。

如果大家能把這三點(diǎn)理解清楚了梯码,那么協(xié)程的異常處理可以說(shuō)就非常清晰了宝泵。文中因?yàn)楫惓鞑サ脑颍覀兲岬搅巳∠ⅲ珱](méi)有展開(kāi)詳細(xì)討論儿奶,后面我們將會(huì)專(zhuān)門(mén)針對(duì)取消輸出一篇文章,幫助大家加深理解鳄抒。

附加說(shuō)明

join 在父協(xié)程被取消時(shí)有一個(gè) bug 會(huì)導(dǎo)致不拋出取消異常闯捎,我在準(zhǔn)備本文時(shí)發(fā)現(xiàn)該問(wèn)題椰弊,目前已經(jīng)提交到官方并得到了修復(fù),預(yù)計(jì)合入到 1.2.1 發(fā)版瓤鼻,大家有興趣可以查看這個(gè) issue:No CancellationException thrown when join on a crashed Job秉版。

當(dāng)然,這個(gè) bug 對(duì)于生成環(huán)境的影響很小娱仔,大家也不要擔(dān)心沐飘。


歡迎關(guān)注 Kotlin 中文社區(qū)游桩!

中文官網(wǎng):https://www.kotlincn.net/

中文官方博客:https://www.kotliner.cn/

公眾號(hào):Kotlin

知乎專(zhuān)欄:Kotlin

CSDN:Kotlin中文社區(qū)

掘金:Kotlin中文社區(qū)

簡(jiǎn)書(shū):Kotlin中文社區(qū)

開(kāi)發(fā)者頭條:Kotlin中文社區(qū)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末牲迫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子借卧,更是在濱河造成了極大的恐慌盹憎,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铐刘,死亡現(xiàn)場(chǎng)離奇詭異陪每,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)镰吵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)檩禾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人疤祭,你說(shuō)我怎么就攤上這事盼产。” “怎么了勺馆?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵戏售,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我草穆,道長(zhǎng)灌灾,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任悲柱,我火速辦了婚禮锋喜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘豌鸡。我一直安慰自己嘿般,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布直颅。 她就那樣靜靜地躺著博个,像睡著了一般。 火紅的嫁衣襯著肌膚如雪功偿。 梳的紋絲不亂的頭發(fā)上盆佣,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天往堡,我揣著相機(jī)與錄音,去河邊找鬼共耍。 笑死虑灰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的痹兜。 我是一名探鬼主播穆咐,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼字旭!你這毒婦竟也來(lái)了对湃?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤遗淳,失蹤者是張志新(化名)和其女友劉穎拍柒,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體屈暗,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拆讯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了养叛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片种呐。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖弃甥,靈堂內(nèi)的尸體忽然破棺而出爽室,到底是詐尸還是另有隱情,我是刑警寧澤潘飘,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布肮之,位于F島的核電站,受9級(jí)特大地震影響卜录,放射性物質(zhì)發(fā)生泄漏戈擒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一艰毒、第九天 我趴在偏房一處隱蔽的房頂上張望筐高。 院中可真熱鬧,春花似錦丑瞧、人聲如沸柑土。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)稽屏。三九已至,卻和暖如春西乖,著一層夾襖步出監(jiān)牢的瞬間狐榔,已是汗流浹背坛增。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留薄腻,地道東北人收捣。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像庵楷,于是被迫代替她去往敵國(guó)和親罢艾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容