Kotlin 協程的異常處理準則
Kotlin 協程的異常處理
概述
協程是互相協作的程序,協程是結構化的。
正是因為協程的這兩個特點,導致它和 Java 的異常處理機制不一樣。如果將 Java 的異常處理機制照搬到Kotlin協程中,會遇到很多問題,如:協程無法取消、try-catch不起作用等。
Kotlin協程中的異常主要分兩大類
- 協程取消異常(CancellationException)
- 其他異常
異常處理六大準則
- 協程的取消需要內部配合。
- 不要打破協程的父子結構。
- 捕獲 CancellationException 異常后,需要考慮是否重新拋出來。
- 不要用 try-catch 直接包裹 launch、async。
- 使用 SurpervisorJob 控制異常傳播的范圍。
- 使用 CoroutineExceptionHandler 處理復雜結構的協程異常,僅在頂層協程中起作用。
核心理念:協程是結構化的,異常傳播也是結構化的。
準則一:協程的取消需要內部配合
協程任務被取消時,它的內部會產生一個 CancellationException 異常,協程的結構化并發(fā)的特點:如果取消了父協程,則子協程也會跟著取消。
問題:cancel不被響應
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 // 不會停止,一直打印輸出 */
原因:協程是相互協作的程序,因此協程任務的取消也需要相互協作。協程外部取消,協程內部需要做出相應。
解決:使用 isActive 判斷是否處于活躍狀態(tài)
使用 isActive 判斷協程的活躍狀態(tài)。
fun main() = runBlocking { val job = launch(Dispatchers.Default) { var i = 0 // 關鍵 // ↓ while (isActive) { Thread.sleep(500L) i++ println("i: $i") } } delay(200L) job.cancel() job.join() println("End") } /* 輸出信息: i: 1 End */
準則二:不要打破協程的父子結構
問題:子協程不會跟隨父協程一起取消
val fixedDispatcher = Executors.newFixedThreadPool(2) { Thread(it, "MyFixedThread") }.asCoroutineDispatcher() fun main() = runBlocking { // 父協程 val parentJob = launch(fixedDispatcher) { //子協程1 launch(Job()) { var i = 0 while (isActive) { Thread.sleep(500L) i++ println("子協程1 i:$i") } } //子協程2 launch { var i = 0 while (isActive) { Thread.sleep(500L) i++ println("子協程2 i:$i") } } } delay(1000L) parentJob.cancel() parentJob.join() println("End") } /* 輸出信息: 子協程1 i:1 子協程2 i:1 子協程2 i:2 子協程1 i:2 End 子協程1 i:3 子協程1 i:4 子協程1 i:5 // 子協程1一直在執(zhí)行,不會停下來 */
原因:協程是結構化的,取消啦父協程,子協程也會被取消。但是在這里“子協程1”不在 parentJob 的子協程,打破了原有的結構化關系,當調用 parentJob.cancel 時,“子協程1”就不會被取消了。
解決:不破壞父子結構
“子協程1”不要傳入額外的 Job()。
fun main() = runBlocking { val parentJob = launch(fixedDispatcher) { launch { var i = 0 while (isActive) { Thread.sleep(500L) i++ println("子協程1:i= $i") } } launch { var i = 0 while (isActive) { Thread.sleep(500L) i++ println("子協程2:i= $i") } } } delay(2000L) parentJob.cancel() parentJob.join() println("end") } /* 輸出結果: 子協程1:i= 1 子協程2:i= 1 子協程2:i= 2 子協程1:i= 2 子協程1:i= 3 子協程2:i= 3 子協程1:i= 4 子協程2:i= 4 end */
準則三:捕獲 CancellationException 需要重新拋出來
掛起函數可以自動響應協程的取消
Kotlin 中的掛起函數是可以自動響應協程的取消,如下中的 delay() 函數可以自動檢測當前協程是否被取消,如果已經取消了它就會拋出一個 CancellationException,從而終止當前協程。
fun main() = runBlocking { // 父協程 val parentJob = launch(Dispatchers.Default) { //子協程1 launch { var i = 0 while (true) { // 這里 delay(500L) i++ println("子協程1 i:$i") } } //子協程2 launch { var i = 0 while (true) { // 這里 delay(500L) i++ println("子協程2 i:$i") } } } delay(1000L) parentJob.cancel() parentJob.join() println("End") } /* 輸出信息: 子協程1 i:1 子協程2 i:1 子協程1 i:2 子協程2 i:2 End */
fun main() = runBlocking { // 父協程 val parentJob = launch(Dispatchers.Default) { //子協程1 launch { var i = 0 while (true) { try { delay(500L) } catch (e: CancellationException) { println("捕獲CancellationException") throw e } i++ println("子協程1 i:$i") } } //子協程2 launch { var i = 0 while (true) { try { delay(500L) } catch (e: CancellationException) { println("捕獲CancellationException") throw e } i++ println("子協程2 i:$i") } } } delay(1000L) parentJob.cancel() parentJob.join() println("End") } /* 輸出信息: 子協程1 i:1 子協程2 i:1 捕獲CancellationException 捕獲CancellationException End */
問題:捕獲 CancellationException 導致崩潰
fun main() = runBlocking { val parentJob = launch(Dispatchers.Default) { launch { var i = 0 while (true) { try { delay(500L) } catch (e: CancellationException) { println("捕獲CancellationException異常") } i++ println("子協程1 i= $i") } } launch { var i = 0 while (true) { delay(500L) i++ println("子協程2 i= $i") } } } delay(2000L) parentJob.cancel() parentJob.join() println("end") } /* 輸出信息: 子協程1 i= 1 子協程2 i= 1 子協程1 i= 2 子協程2 i= 2 子協程1 i= 3 子協程2 i= 3 捕獲CancellationException異常 ...... //程序不會終止 */
原因:當捕獲到 CancellationException 以后,還需要將它重新拋出去,如果沒有拋出去則子協程將無法取消。
解決:需要重新拋出
重新拋出異常,執(zhí)行 throw e
。
以上三條準則,都是應對 CancellationException 這個特殊異常的。
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("協程1 i= $i") } } launch { var i = 0 while (true) { delay(500L) i++ println("協程2 i= $i") } } } delay(2000L) parentJob.cancel() parentJob.join() println("end") } /* 輸出信息: 協程1 i= 1 協程2 i= 1 協程2 i= 2 協程1 i= 2 協程2 i= 3 協程1 i= 3 捕獲CancellationException異常 end */
準則四:不要用try-catch直接包裹launch、async
問題:try-catch不起作用
fun main() = runBlocking { try { launch { delay(100L) 1 / 0 //產生異常 } } catch (e: ArithmeticException) { println("捕獲:$e") } delay(500L) println("end") } /* 輸出信息: Exception in thread "main" java.lang.ArithmeticException: / by zero */
原因:協程的代碼執(zhí)行順序與普通程序不一樣,當協程執(zhí)行 1 / 0
時,程序實際已經跳出 try-catch 的作用域了,所以直接使用 try-catch 包裹 launch、async 是沒有任何效果的。
解決:調整作用域
可以將 try-catch 移動到協程體內部,這樣可以捕獲到異常了。
fun main() = runBlocking { launch { delay(100L) try { 1 / 0 //產生異常 } catch (e: ArithmeticException) { println("捕獲異常:$e") } } delay(500L) println("end") } /* 輸出信息: 捕獲異常:java.lang.ArithmeticException: / by zero end */
準則五:靈活使用SurpervisorJob
問題:子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 時,當子Job發(fā)生異常時,會導致 parentJob 取消,從而導致其他子Job也受到牽連,這也是協程結構化的體現。
解決:使用 SupervisorJob
SurpervisorJob 是 Job 的子類,SurpervisorJob 是一個種特殊的 Job,可以控制異常的傳播范圍,當子Job發(fā)生異常時,其他的子Job不會受到影響。
將 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 */
準則六:使用 CoroutineExceptionHandler 處理復雜結構的協程異常
問題:復雜結構的協程異常
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 */
原因:模擬一個復雜的協程嵌套場景,開發(fā)人員很難在每一個協程體中寫 try-catch,為了捕獲異常,可以使用 CoroutineExceptionHandler。
解決:使用CoroutineExceptionHandler
使用 CoroutineExceptionHandler 處理復雜結構的協程異常,它只能在頂層協程中起作用。
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 */
總結
- 準則一:協程的取消需要內部的配合。
- 準則二:不要輕易打破協程的父子結構。協程的優(yōu)勢在于結構化并發(fā),他的許多特性都是建立在這之上的,如果打破了它的父子結構,會導致協程無法按照預期執(zhí)行。
- 準則三:捕獲 CancellationException 異常后,需要考慮是否重新拋出來。協程是依賴 CancellationException 異常來實現結構化取消的,捕獲異常后需要考慮是否重新拋出來。
- 準則四:不要用 try-catch 直接包裹 launch、async。協程代碼的執(zhí)行順序與普通程序不一樣,直接使用 try-catch 可能不會達到預期效果。
- 準則五:使用 SupervisorJob 控制異常傳播范圍。SupervisorJob 是一種特殊的 Job,可以控制異常的傳播范圍,不會受到子協程中的異常而取消自己。
- 準則六:使用 CoroutineExceptionHandler 捕獲異常。當協程嵌套層級比較深時,可以在頂層協程中定義 CoroutineExceptionHandler 捕獲整個作用域的所有異常。
到此這篇關于Kotlin 協程的異常處理的文章就介紹到這了,更多相關Kotlin 協程的異常處理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android使用AlertDialog實現的信息列表單選、多選對話框功能
在使用AlertDialog實現單選和多選對話框時,分別設置setSingleChoiceItems()和setMultiChoiceItems()函數。具體實現代碼大家參考下本文吧2017-03-03android 仿微信demo——微信消息界面實現(移動端)
本系列文章主要介紹了微信小程序-閱讀小程序實例(demo),小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望能給你們提供幫助2021-06-06Android使用Realm數據庫實現App中的收藏功能(代碼詳解)
這篇文章主要介紹了Android使用Realm數據庫實現App中的收藏功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03Android自定義ViewGroup實現絢麗的仿支付寶咻一咻雷達脈沖效果
這篇文章主要介紹了Android自定義ViewGroup實現絢麗的仿支付寶咻一咻雷達脈沖效果的相關資料,需要的朋友可以參考下2016-10-10android中實現在ImageView上隨意畫線涂鴉的方法
今天小編就為大家分享一篇android中實現在ImageView上隨意畫線涂鴉的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-10-10