關(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è)谥vDispatchers.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
司志、actor
和 produce
甜紫,其中 actor
和 launch
的行為類(lèi)似,在未捕獲的異常出現(xiàn)以后骂远,會(huì)被當(dāng)做為處理的異常拋出囚霸,就像前面的例子那樣。而 async
和 produce
則主要是用來(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í)拋出異常。
actor
和produce
這兩個(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ì)理一下主要有三條線:
- 協(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í)將異常拋出狰域。
- 異常在作用域內(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ì)向上傳遞,所謂“自作自受”场航。
- 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ū)
簡(jiǎn)書(shū):Kotlin中文社區(qū)
開(kāi)發(fā)者頭條:Kotlin中文社區(qū)