Kotlin 協(xié)程的異常處理準(zhǔn)則
Kotlin 協(xié)程的異常處理
概述
協(xié)程是互相協(xié)作的程序,協(xié)程是結(jié)構(gòu)化的。
正是因?yàn)閰f(xié)程的這兩個(gè)特點(diǎn),導(dǎo)致它和 Java 的異常處理機(jī)制不一樣。如果將 Java 的異常處理機(jī)制照搬到Kotlin協(xié)程中,會(huì)遇到很多問(wèn)題,如:協(xié)程無(wú)法取消、try-catch不起作用等。
Kotlin協(xié)程中的異常主要分兩大類
- 協(xié)程取消異常(CancellationException)
- 其他異常
異常處理六大準(zhǔn)則
- 協(xié)程的取消需要內(nèi)部配合。
- 不要打破協(xié)程的父子結(jié)構(gòu)。
- 捕獲 CancellationException 異常后,需要考慮是否重新拋出來(lái)。
- 不要用 try-catch 直接包裹 launch、async。
- 使用 SurpervisorJob 控制異常傳播的范圍。
- 使用 CoroutineExceptionHandler 處理復(fù)雜結(jié)構(gòu)的協(xié)程異常,僅在頂層協(xié)程中起作用。
核心理念:協(xié)程是結(jié)構(gòu)化的,異常傳播也是結(jié)構(gòu)化的。
準(zhǔn)則一:協(xié)程的取消需要內(nèi)部配合
協(xié)程任務(wù)被取消時(shí),它的內(nèi)部會(huì)產(chǎn)生一個(gè) CancellationException 異常,協(xié)程的結(jié)構(gòu)化并發(fā)的特點(diǎn):如果取消了父協(xié)程,則子協(xié)程也會(huì)跟著取消。
問(wèn)題:cancel不被響應(yīng)
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
while (true) {
Thread.sleep(500L)
i++
println("i: $i")
}
}
delay(200L)
job.cancel()
job.join()
println("End")
}
/*
輸出信息:
i: 1
i: 2
i: 3
i: 4
// 不會(huì)停止,一直打印輸出
*/原因:協(xié)程是相互協(xié)作的程序,因此協(xié)程任務(wù)的取消也需要相互協(xié)作。協(xié)程外部取消,協(xié)程內(nèi)部需要做出相應(yīng)。
解決:使用 isActive 判斷是否處于活躍狀態(tài)
使用 isActive 判斷協(xié)程的活躍狀態(tài)。
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
var i = 0
// 關(guān)鍵
// ↓
while (isActive) {
Thread.sleep(500L)
i++
println("i: $i")
}
}
delay(200L)
job.cancel()
job.join()
println("End")
}
/*
輸出信息:
i: 1
End
*/準(zhǔn)則二:不要打破協(xié)程的父子結(jié)構(gòu)
問(wèn)題:子協(xié)程不會(huì)跟隨父協(xié)程一起取消
val fixedDispatcher = Executors.newFixedThreadPool(2) {
Thread(it, "MyFixedThread")
}.asCoroutineDispatcher()
fun main() = runBlocking {
// 父協(xié)程
val parentJob = launch(fixedDispatcher) {
//子協(xié)程1
launch(Job()) {
var i = 0
while (isActive) {
Thread.sleep(500L)
i++
println("子協(xié)程1 i:$i")
}
}
//子協(xié)程2
launch {
var i = 0
while (isActive) {
Thread.sleep(500L)
i++
println("子協(xié)程2 i:$i")
}
}
}
delay(1000L)
parentJob.cancel()
parentJob.join()
println("End")
}
/*
輸出信息:
子協(xié)程1 i:1
子協(xié)程2 i:1
子協(xié)程2 i:2
子協(xié)程1 i:2
End
子協(xié)程1 i:3
子協(xié)程1 i:4
子協(xié)程1 i:5
// 子協(xié)程1一直在執(zhí)行,不會(huì)停下來(lái)
*/原因:協(xié)程是結(jié)構(gòu)化的,取消啦父協(xié)程,子協(xié)程也會(huì)被取消。但是在這里“子協(xié)程1”不在 parentJob 的子協(xié)程,打破了原有的結(jié)構(gòu)化關(guān)系,當(dāng)調(diào)用 parentJob.cancel 時(shí),“子協(xié)程1”就不會(huì)被取消了。
解決:不破壞父子結(jié)構(gòu)
“子協(xié)程1”不要傳入額外的 Job()。
fun main() = runBlocking {
val parentJob = launch(fixedDispatcher) {
launch {
var i = 0
while (isActive) {
Thread.sleep(500L)
i++
println("子協(xié)程1:i= $i")
}
}
launch {
var i = 0
while (isActive) {
Thread.sleep(500L)
i++
println("子協(xié)程2:i= $i")
}
}
}
delay(2000L)
parentJob.cancel()
parentJob.join()
println("end")
}
/*
輸出結(jié)果:
子協(xié)程1:i= 1
子協(xié)程2:i= 1
子協(xié)程2:i= 2
子協(xié)程1:i= 2
子協(xié)程1:i= 3
子協(xié)程2:i= 3
子協(xié)程1:i= 4
子協(xié)程2:i= 4
end
*/準(zhǔn)則三:捕獲 CancellationException 需要重新拋出來(lái)
掛起函數(shù)可以自動(dòng)響應(yīng)協(xié)程的取消
Kotlin 中的掛起函數(shù)是可以自動(dòng)響應(yīng)協(xié)程的取消,如下中的 delay() 函數(shù)可以自動(dòng)檢測(cè)當(dāng)前協(xié)程是否被取消,如果已經(jīng)取消了它就會(huì)拋出一個(gè) CancellationException,從而終止當(dāng)前協(xié)程。
fun main() = runBlocking {
// 父協(xié)程
val parentJob = launch(Dispatchers.Default) {
//子協(xié)程1
launch {
var i = 0
while (true) {
// 這里
delay(500L)
i++
println("子協(xié)程1 i:$i")
}
}
//子協(xié)程2
launch {
var i = 0
while (true) {
// 這里
delay(500L)
i++
println("子協(xié)程2 i:$i")
}
}
}
delay(1000L)
parentJob.cancel()
parentJob.join()
println("End")
}
/*
輸出信息:
子協(xié)程1 i:1
子協(xié)程2 i:1
子協(xié)程1 i:2
子協(xié)程2 i:2
End
*/fun main() = runBlocking {
// 父協(xié)程
val parentJob = launch(Dispatchers.Default) {
//子協(xié)程1
launch {
var i = 0
while (true) {
try {
delay(500L)
} catch (e: CancellationException) {
println("捕獲CancellationException")
throw e
}
i++
println("子協(xié)程1 i:$i")
}
}
//子協(xié)程2
launch {
var i = 0
while (true) {
try {
delay(500L)
} catch (e: CancellationException) {
println("捕獲CancellationException")
throw e
}
i++
println("子協(xié)程2 i:$i")
}
}
}
delay(1000L)
parentJob.cancel()
parentJob.join()
println("End")
}
/*
輸出信息:
子協(xié)程1 i:1
子協(xié)程2 i:1
捕獲CancellationException
捕獲CancellationException
End
*/問(wèn)題:捕獲 CancellationException 導(dǎo)致崩潰
fun main() = runBlocking {
val parentJob = launch(Dispatchers.Default) {
launch {
var i = 0
while (true) {
try {
delay(500L)
} catch (e: CancellationException) {
println("捕獲CancellationException異常")
}
i++
println("子協(xié)程1 i= $i")
}
}
launch {
var i = 0
while (true) {
delay(500L)
i++
println("子協(xié)程2 i= $i")
}
}
}
delay(2000L)
parentJob.cancel()
parentJob.join()
println("end")
}
/*
輸出信息:
子協(xié)程1 i= 1
子協(xié)程2 i= 1
子協(xié)程1 i= 2
子協(xié)程2 i= 2
子協(xié)程1 i= 3
子協(xié)程2 i= 3
捕獲CancellationException異常
...... //程序不會(huì)終止
*/原因:當(dāng)捕獲到 CancellationException 以后,還需要將它重新拋出去,如果沒(méi)有拋出去則子協(xié)程將無(wú)法取消。
解決:需要重新拋出
重新拋出異常,執(zhí)行 throw e。
以上三條準(zhǔn)則,都是應(yīng)對(duì) CancellationException 這個(gè)特殊異常的。
fun main() = runBlocking {
val parentJob = launch(Dispatchers.Default) {
launch {
var i = 0
while (true) {
try {
delay(500L)
} catch (e: CancellationException) {
println("捕獲CancellationException異常")
// 拋出異常
throw e
}
i++
println("協(xié)程1 i= $i")
}
}
launch {
var i = 0
while (true) {
delay(500L)
i++
println("協(xié)程2 i= $i")
}
}
}
delay(2000L)
parentJob.cancel()
parentJob.join()
println("end")
}
/*
輸出信息:
協(xié)程1 i= 1
協(xié)程2 i= 1
協(xié)程2 i= 2
協(xié)程1 i= 2
協(xié)程2 i= 3
協(xié)程1 i= 3
捕獲CancellationException異常
end
*/準(zhǔn)則四:不要用try-catch直接包裹launch、async
問(wèn)題:try-catch不起作用
fun main() = runBlocking {
try {
launch {
delay(100L)
1 / 0 //產(chǎn)生異常
}
} catch (e: ArithmeticException) {
println("捕獲:$e")
}
delay(500L)
println("end")
}
/*
輸出信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
*/原因:協(xié)程的代碼執(zhí)行順序與普通程序不一樣,當(dāng)協(xié)程執(zhí)行 1 / 0 時(shí),程序?qū)嶋H已經(jīng)跳出 try-catch 的作用域了,所以直接使用 try-catch 包裹 launch、async 是沒(méi)有任何效果的。
解決:調(diào)整作用域
可以將 try-catch 移動(dòng)到協(xié)程體內(nèi)部,這樣可以捕獲到異常了。
fun main() = runBlocking {
launch {
delay(100L)
try {
1 / 0 //產(chǎn)生異常
} catch (e: ArithmeticException) {
println("捕獲異常:$e")
}
}
delay(500L)
println("end")
}
/*
輸出信息:
捕獲異常:java.lang.ArithmeticException: / by zero
end
*/準(zhǔn)則五:靈活使用SurpervisorJob
問(wèn)題:子Job發(fā)生異常影響其他子Job
fun main() = runBlocking {
launch {
launch {
1 / 0
delay(100L)
println("hello world 111")
}
launch {
delay(200L)
println("hello world 222")
}
launch {
delay(300L)
println("hello world 333")
}
}
delay(1000L)
println("end")
}
/*
輸出信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
*/原因:使用普通 Job 時(shí),當(dāng)子Job發(fā)生異常時(shí),會(huì)導(dǎo)致 parentJob 取消,從而導(dǎo)致其他子Job也受到牽連,這也是協(xié)程結(jié)構(gòu)化的體現(xiàn)。
解決:使用 SupervisorJob
SurpervisorJob 是 Job 的子類,SurpervisorJob 是一個(gè)種特殊的 Job,可以控制異常的傳播范圍,當(dāng)子Job發(fā)生異常時(shí),其他的子Job不會(huì)受到影響。

將 parentJob 改為 SupervisorJob。
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
scope.launch {
1 / 0
delay(100L)
println("hello world 111")
}
scope.launch {
delay(200L)
println("hello world 222")
}
scope.launch {
delay(300L)
println("hello world 333")
}
delay(1000L)
println("end")
}
/*
輸出信息:
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.ArithmeticException: / by zero
hello world 222
hello world 333
end
*/解決:使用 supervisorScope
supervisorScope 底層依然使用的是 SupervisorJob。
fun main() = runBlocking {
supervisorScope {
launch {
1 / 0
delay(100L)
println("hello world 111")
}
launch {
delay(200L)
println("hello world 222")
}
launch {
delay(300L)
println("hello world 333")
}
}
delay(1000L)
println("end")
}
/*
輸出信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
hello world 222
hello world 333
end
*/準(zhǔn)則六:使用 CoroutineExceptionHandler 處理復(fù)雜結(jié)構(gòu)的協(xié)程異常
問(wèn)題:復(fù)雜結(jié)構(gòu)的協(xié)程異常
fun main() = runBlocking {
val scope = CoroutineScope(coroutineContext)
scope.launch {
async { delay(100L) }
launch {
delay(100L)
launch {
delay(100L)
1 / 0
}
}
delay(100L)
}
delay(1000L)
println("end")
}
/*
輸出信息:
Exception in thread "main" java.lang.ArithmeticException: / by zero
*/原因:模擬一個(gè)復(fù)雜的協(xié)程嵌套場(chǎng)景,開(kāi)發(fā)人員很難在每一個(gè)協(xié)程體中寫 try-catch,為了捕獲異常,可以使用 CoroutineExceptionHandler。
解決:使用CoroutineExceptionHandler
使用 CoroutineExceptionHandler 處理復(fù)雜結(jié)構(gòu)的協(xié)程異常,它只能在頂層協(xié)程中起作用。
fun main() = runBlocking {
val myCoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("捕獲異常:$throwable")
}
val scope = CoroutineScope(coroutineContext + Job() + myCoroutineExceptionHandler)
scope.launch {
async { delay(100L) }
launch {
delay(100L)
launch {
delay(100L)
1 / 0
}
}
delay(100L)
}
delay(1000L)
println("end")
}
/*
輸出信息:
捕獲異常:java.lang.ArithmeticException: / by zero
end
*/總結(jié)
- 準(zhǔn)則一:協(xié)程的取消需要內(nèi)部的配合。
- 準(zhǔn)則二:不要輕易打破協(xié)程的父子結(jié)構(gòu)。協(xié)程的優(yōu)勢(shì)在于結(jié)構(gòu)化并發(fā),他的許多特性都是建立在這之上的,如果打破了它的父子結(jié)構(gòu),會(huì)導(dǎo)致協(xié)程無(wú)法按照預(yù)期執(zhí)行。
- 準(zhǔn)則三:捕獲 CancellationException 異常后,需要考慮是否重新拋出來(lái)。協(xié)程是依賴 CancellationException 異常來(lái)實(shí)現(xiàn)結(jié)構(gòu)化取消的,捕獲異常后需要考慮是否重新拋出來(lái)。
- 準(zhǔn)則四:不要用 try-catch 直接包裹 launch、async。協(xié)程代碼的執(zhí)行順序與普通程序不一樣,直接使用 try-catch 可能不會(huì)達(dá)到預(yù)期效果。
- 準(zhǔn)則五:使用 SupervisorJob 控制異常傳播范圍。SupervisorJob 是一種特殊的 Job,可以控制異常的傳播范圍,不會(huì)受到子協(xié)程中的異常而取消自己。
- 準(zhǔn)則六:使用 CoroutineExceptionHandler 捕獲異常。當(dāng)協(xié)程嵌套層級(jí)比較深時(shí),可以在頂層協(xié)程中定義 CoroutineExceptionHandler 捕獲整個(gè)作用域的所有異常。
到此這篇關(guān)于Kotlin 協(xié)程的異常處理的文章就介紹到這了,更多相關(guān)Kotlin 協(xié)程的異常處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android編程之在SD卡上進(jìn)行文件讀寫操作實(shí)例詳解
這篇文章主要介紹了Android編程之在SD卡上進(jìn)行文件讀寫操作的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android的文件操作及針對(duì)SD卡的存取操作相關(guān)技巧,需要的朋友可以參考下2015-12-12
Android SQLite數(shù)據(jù)庫(kù)徹底掌握數(shù)據(jù)存儲(chǔ)
這篇文章主要介紹了 Android SQLite數(shù)據(jù)庫(kù)的相關(guān)資料,這里對(duì)Android SQLlite做了詳細(xì)介紹,需要的朋友可以參考下2016-10-10
Android 判斷某個(gè)服務(wù)(service)是否運(yùn)行
這篇文章主要介紹了 Android 判斷某個(gè)服務(wù)(service)是否運(yùn)行的相關(guān)資料,需要的朋友可以參考下2017-06-06
Flutter?DateTime日期轉(zhuǎn)換的詳細(xì)使用
本文主要介紹了Flutter?DateTime日期轉(zhuǎn)換的詳細(xì)使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
Android 服務(wù)端將位置信息發(fā)送給客戶端的實(shí)現(xiàn)
這篇文章主要介紹了Android 服務(wù)端將位置信息發(fā)送給客戶端的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
Android開(kāi)發(fā)教程之shape和selector的結(jié)合使用
shape和selector是Android UI設(shè)計(jì)中經(jīng)常用到的,比如我們要自定義一個(gè)圓角Button,點(diǎn)擊Button有些效果的變化,就要用到shape和selector,接下來(lái)通過(guò)本文給大家介紹Android開(kāi)發(fā)教程之shape和selector的結(jié)合使用,感興趣的朋友一起學(xué)習(xí)吧2016-01-01
Android實(shí)現(xiàn)基本功能的新聞應(yīng)用
這篇文章主要介紹了一個(gè)簡(jiǎn)易功能的Android新聞應(yīng)用實(shí)現(xiàn)流程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-12-12
Android實(shí)現(xiàn) EditText輸入手機(jī)號(hào)空格功能
這篇文章主要介紹了Android實(shí)現(xiàn) EditText輸入手機(jī)號(hào)空格功能,實(shí)現(xiàn)思路是要重寫TextWatcher,每次EditText內(nèi)容變化,都判斷內(nèi)容是否符合要求,具體實(shí)例代碼大家參考下本文2018-02-02

