前言
本文全面解釋協(xié)程的異常傳遞機(jī)制以及處理方式鹰祸,需要一定的協(xié)程基礎(chǔ)。擺脫只會(huì)使用 try catch 的尷尬取逾,以更優(yōu)雅和更靈活的方式處理異常逆趣。
異常傳遞
Job
對(duì)于普通 Job
來(lái)說(shuō)荆几,異常的傳遞是雙向的吓妆,即異常會(huì)向子協(xié)程和父協(xié)程傳播,流程為:
- 當(dāng)前協(xié)程出現(xiàn)異常
-
cancel
子協(xié)程 - 等待子協(xié)程
cancel
完成吨铸,cancel
自己 - 傳遞給父協(xié)程并循環(huán)前面步驟
可見行拢,異常會(huì)自下而上地傳播,任一協(xié)程發(fā)生異常會(huì)影響到整個(gè)協(xié)程樹诞吱。
如下代碼中舟奠,child 1出現(xiàn)異常后,child 3房维、child 2沼瘫、child 0 依次會(huì)被取消。
CoroutineScope(Job()).launch { // child 0
launch {// child 1
launch { // child 3
delay(1000)
println("child 3")
}
throw Exception("test")
}
launch { // child 2
delay(1000)
println("child 2")
}
delay(1000)
println("child 0")
}.join()
SupervisorJob
如果不希望協(xié)程內(nèi)的異常向上傳播或影響同級(jí)協(xié)程握巢≡稳担可以使用 SupervisorJob
松却。
SuperVisorJob
可以使子協(xié)程的異常向上傳播到 SupervisorJob
層時(shí)不被處理暴浦,即異常向上傳播在 SupervisorJob
處終止。
如上圖所示晓锻,SupervisorJob
或 Scope
的任一直接子協(xié)程發(fā)生異常都不會(huì)影響其他直接子協(xié)程歌焦,更不會(huì)向上傳播影響父協(xié)程。
注意砚哆,父協(xié)程異常依然會(huì)導(dǎo)致子協(xié)程取消独撇,這點(diǎn)和
Job
一致。
CoroutineScope(Job()).launch { // child 0
val supervisorJob = SupervisorJob()
launch(supervisorJob) {// child 1
launch { // child 3
delay(1000)
println("child 3")
}
throw Exception("test")
}
launch(supervisorJob) { // child 2
delay(1000)
println("child 2")
}
delay(1000)
println("child 0")
}.join()
還是上面的例子躁锁,使用 SupervisorJob
作為 child 1 和 child 2 的父 Job纷铣,這樣 child 2 中的異常只能向下影響 child 3 ,避免了異常影響到 child 0 和 child 2战转。
也可以使用 supervisorScope
搜立,它是一個(gè)自帶了 SupervisorJob
的協(xié)程作用域,效果是一樣的:
CoroutineScope(Job()).launch { // child 0
supervisorScope {
launch {// child 1
launch { // child 3
delay(1000)
println("child 3")
}
throw Exception("test")
}
launch { // child 2
delay(1000)
println("child 2")
}
}
delay(1000)
println("child 0")
}.join()
不同的是槐秧,supervisorScope
是掛起方法啄踊,會(huì)掛起協(xié)程直至所有子協(xié)程執(zhí)行完成。
注意刁标,易錯(cuò) ??
下面代碼中颠通,誰(shuí)是 Child1 的父協(xié)程?
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
launch {
// Child 1
}
launch {
// Child 2
}
}
Child 的父協(xié)程類型是 Job
膀懈,并不是 SupervisorJob
顿锰!
scope.launch
會(huì)創(chuàng)建一個(gè)新 Job
,該 Job
的父 Job 是 launch 時(shí)指定的,所以 SupervisorJob
是新 Job
的父 Job 硼控,而 scope 創(chuàng)建時(shí)傳入的 Job 被 SupervisorJob
覆蓋乘客,因而協(xié)程關(guān)系為:
SupervisorJob -〉Job -〉(child1、child2)淀歇,因而上述代碼中易核,SupervisorJob
沒(méi)有起到該有的作用。
鑒于上述原因浪默,可以:
val scope = CoroutineScope(SupervisorJob())
scope.launch {
// Child 1
}
scope.launch {
// Child 2
}
也可以使用 supervisorScope
達(dá)到預(yù)期效果:
supervisorScope {
launch { // Child 1
throw IllegalArgumentException()
}
launch { // Child 2
delay(1000)
println("child 2 run over") //可順利執(zhí)行完成
}
delay(1000)
println("Job: ${coroutineContext[Job]?.javaClass}")
}
打印結(jié)果:
Exception in thread "Test worker @coroutine#2" java.lang.IllegalArgumentException
Job: class kotlinx.coroutines.SupervisorCoroutine
child 2 run over
不管是什么 Job牡直,異常傳遞如果不做處理,最終會(huì)到達(dá)線程的 UncaughtExceptionHandler纳决,如果是 JVM 則輸出在控制臺(tái)碰逸,如果是 Android 沒(méi)設(shè)置 UncaughtExceptionHandler 則會(huì)出現(xiàn) app 崩潰(如果是主線程則一定崩潰)。
異常處理
對(duì)于不同協(xié)程構(gòu)造器阔加,異常的處理方式不同饵史。分別介紹 launch
和 async
情況下的異常處理。
Launch
-
try catch
launch 方式啟動(dòng)的協(xié)程胜榔,異常會(huì)在發(fā)生時(shí)立刻拋出胳喷,使用
try catch
就可以將協(xié)程中的異常捕獲。如:scope.launch { try { codeThatCanThrowExceptions() } catch(e: Exception) { // Handle exception } }
try catch
整個(gè)協(xié)程也是可以的:try { coroutineScope { codeThatCanThrowExceptions() } } catch (t: Throwable) { // Handle exception }
注意夭织,這樣是不可以的 ? :
try { CoroutineScope().launch { codeThatCanThrowExceptions() } } catch (t: Throwable) { // Handle exception }
因?yàn)?launch 不是掛起函數(shù)吭露。
-
CoroutineExceptionHandler
除了使用
try catch
,更推薦使用CoroutineExceptionHandler
對(duì)異常進(jìn)行統(tǒng)一處理尊惰。需要注意的是異常會(huì)層層代理到根協(xié)程讲竿,所以CoroutineExceptionHandler
只能在根協(xié)程中才能生效。比如這樣 ? :
CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex successfully") }) { throw RuntimeException() }
或者這樣 ? 弄屡,根協(xié)程會(huì)繼承該
CoroutineExceptionHandler
:CoroutineScope(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex successfully") }).launch() { throw RuntimeException() }
而這樣是不對(duì)的 ? :
coroutineScope { launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex failed") }) { throw RuntimeException() } }
子協(xié)程異常會(huì)代理給父協(xié)程题禀,一直向上傳遞直到根協(xié)程,如果找到
CoroutineExceptionHandler
則處理膀捷,否則走UncaughtExceptionHandler
迈嘹。可見担孔,其他子協(xié)程中的CoroutineExceptionHandler
不會(huì)起到作用江锨。猜猜被誰(shuí)捕獲?:
CoroutineScope(Job() + CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex in scope") }).launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex in top Coroutine") }) { throw RuntimeException() }
結(jié)論:scope 中的
CoroutineExceptionHandler
會(huì)覆蓋糕篇。這種將異常代理給父協(xié)程的行為可以被
SupervisorJob
改變啄育,將異常交給子協(xié)程自己處理:supervisorScope { launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("catch ex successfully") }) { throw RuntimeException() } }
異常傳遞到
SupervisorJob
處停止,交由SupervisorJob
的直接子協(xié)程處理拌消,這時(shí)CoroutineExceptionHandler
是生效的挑豌。根協(xié)程:由
scope
直接調(diào)用launch
或async
開啟的協(xié)程安券。
Async
-
try catch
當(dāng)
async
開啟的協(xié)程為根協(xié)程,或SupervisorJob
的直接子協(xié)程時(shí)氓英,異常在調(diào)用await
時(shí)拋出侯勉,使用try catch
可以捕獲異常:/** * async 開啟的協(xié)程為根協(xié)程 */ fun main() = runBlocking { val deferred = GlobalScope.async { throw Exception() } try { deferred.await() //拋出異常 } catch (t: Throwable) { println("捕獲異常:$t") } }
/** * async 開啟的協(xié)程為 SupervisorJob 的直接子協(xié)程 */ fun main() = runBlocking { supervisorScope { val deferred = async { throw Exception() } try { deferred.await() //拋出異常 } catch (t: Throwable) { println("捕獲異常:$t") } } }
/** * async 開啟的協(xié)程為 SupervisorJob 的直接子協(xié)程 */ CoroutineScope(Job()).launch { val deferred = async(SupervisorJob()) { throw Exception() } try { deferred.await() //拋出異常 } catch (t: Throwable) { println("捕獲異常:$t") } }
-
CoroutineExceptionHandler
當(dāng):
“
async
開啟的協(xié)程為根協(xié)程 或supervisorScope
的直接子協(xié)程”的條件不成立時(shí),異常會(huì)在發(fā)生時(shí)立刻拋出并傳播铝阐,對(duì)
await
進(jìn)行try catch
就不起作用了:fun main(): Unit = runBlocking { supervisorScope { launch { val deferred = async { throw Exception() } //拋出異常 try { deferred.await() } catch (e: Exception) { println("catch ex") } delay(1000) println("done") } } }
控制臺(tái)中雖然打印了 “catch ex”址貌,但未能打印 “done”,這表明異常傳遞到了父協(xié)程徘键,父協(xié)程被取消练对。至于為什么還能打印 "catch ex",是因?yàn)?deferred.await() 會(huì)拋出異常吹害,async 中的異趁荆總會(huì)在 await 時(shí)拋出。如果在 await 前加個(gè) delay它呀,那么就看不到 "catch ex" 了螺男,因?yàn)?await 被取消了。
可以使用
CoroutineExceptionHandler
進(jìn)行處理纵穿。CoroutineExceptionHandler
只在根協(xié)程和SupervisorJob
的直接子協(xié)程有效下隧。因此需要在launch
開啟的父協(xié)程進(jìn)行處理:/** * async 開啟的協(xié)程非 supervisorScope 的直接子協(xié)程,異常會(huì)直接拋出政恍, * try catch await 無(wú)效汪拥,但可由 CoroutineExceptionHandler 處理 */ fun main(): Unit = runBlocking { supervisorScope { launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("CoroutineExceptionHandler 捕獲異常:$throwable") }) { val deferred = async { throw Exception() } //拋出異常 deferred.await() } } }
/** * async 開啟的協(xié)程非根協(xié)程,異常會(huì)直接拋出篙耗, * try catch await 無(wú)效,但可由 CoroutineExceptionHandler 處理 */ fun main(): Unit = runBlocking { CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable -> println("CoroutineExceptionHandler 捕獲異常:$throwable") }) { val deferred = async { throw Exception() } //拋出異常 deferred.await() }.join() }
總的來(lái)說(shuō)宪赶,不管是 launch 還是 async宗弯,使用 CoroutineExceptionHandler
的規(guī)則都是一致的,也更不易出錯(cuò)搂妻,推薦使用蒙保。
Cancellation 和 異常
CancellationException 總會(huì)被 CoroutineExceptionHandler 忽略,但能被 try catch 捕獲欲主,ok邓厕,又多了個(gè)使用 CoroutineExceptionHandler
的理由。
fun main() = runBlocking {
CoroutineScope(Job()).launch(CoroutineExceptionHandler { coroutineContext, throwable ->
println("CoroutineExceptionHandler 捕獲異常:$throwable")
}) {
println("運(yùn)行了")
cancel()
try {
delay(1000)
} catch (throwable: Throwable) {
println("try catch 捕獲異常:$throwable")
throw throwable
}
}.join()
}
運(yùn)行結(jié)果:
運(yùn)行了
try catch 捕獲異常:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@54cda7ca
異常聚合
如果一個(gè)協(xié)程拋出不止一個(gè)異常扁瓢,那么則以第一個(gè)拋出的異常為主详恼,其他異常作為 suppressed
異常依附在主異常中。
源碼
關(guān)于異常的傳播控制邏輯可參見 JobSupport.kt
類的 notifyCancelling
和 childCancelled
方法引几,SupervisorJob
就是復(fù)寫了 childCancelled
方法才阻止了異常的向上傳播昧互。
結(jié)構(gòu)化并發(fā)
它是一種編程范式,旨在通過(guò)結(jié)構(gòu)化的方式使并發(fā)編程 更清晰明確、更高質(zhì)量敞掘、更易維護(hù)叽掘。
其核心有幾點(diǎn):
- 通過(guò)把多線程任務(wù)進(jìn)行結(jié)構(gòu)化的包裝,使其具有明確的開始和結(jié)束點(diǎn)玖雁,并確保其孵化出的所有任務(wù)在退出前全部完成更扁。
- 這種包裝允許結(jié)構(gòu)中線程發(fā)生的異常能夠傳播至結(jié)構(gòu)頂端的作用域,并且能夠被該語(yǔ)言原生異常機(jī)制捕獲赫冬。
kotlin 協(xié)程設(shè)計(jì)中的協(xié)程關(guān)系疯潭、執(zhí)行順序、異常傳播/處理 都符合結(jié)構(gòu)化并發(fā)面殖。結(jié)構(gòu)化并發(fā)明確了并發(fā)任務(wù)什么時(shí)候開始竖哩,什么時(shí)候結(jié)束,異常如何傳播脊僚,通過(guò)控制頂層結(jié)構(gòu)具柄就可實(shí)現(xiàn)整個(gè)并發(fā)結(jié)構(gòu)的取消相叁、異常處理,使復(fù)雜的并發(fā)問(wèn)題簡(jiǎn)單辽幌、清晰增淹、可控∥谄螅可以說(shuō)虑润,結(jié)構(gòu)化并發(fā)大大降低了并發(fā)編程的難度。
總結(jié)
- 協(xié)程中未捕獲的異臣咏停總會(huì)向下取消子協(xié)程拳喻,向上傳遞異常,體現(xiàn)了結(jié)構(gòu)化并發(fā)的特點(diǎn)猪腕。
-
SupervisorJob
可以阻止異常繼續(xù)向上傳播冗澈,并將異常交給子協(xié)程處理。 -
launch
啟動(dòng)的協(xié)程在異常發(fā)生時(shí)總是立刻拋出陋葡,可以由try catch
捕獲亚亲,也可以使用CoroutineExceptionHandler
處理,注意 handler 使用的位置腐缤。 - 而
async
啟動(dòng)的協(xié)程在其為根協(xié)程或supervisorScope
的直接子協(xié)程時(shí)捌归,異常會(huì)在async
內(nèi)部捕獲,當(dāng)deferred
對(duì)象調(diào)用await
時(shí)拋出岭粤,否則也會(huì)在異常發(fā)生時(shí)立刻拋出并傳播惜索。前者可以使用try catch
捕獲await
調(diào)用,當(dāng)然CoroutineExceptionHandler
也可以绍在,而后者不能通過(guò)try catch
整個(gè)async
代碼塊或await
調(diào)用捕獲異常门扇。 - 結(jié)構(gòu)化并發(fā)大大降低了并發(fā)編程的難度雹有,kotlin 協(xié)程設(shè)計(jì)也遵循該編程范式。