在kotlin協(xié)程中如何正確處理異常

在簡單的kotlin中的異常處理

try {
    // some code
    throw RuntimeException("RuntimeException in 'some code'")
} catch (exception: Exception) {
    println("Handle $exception")
}

// Output:
// Handle java.lang.RuntimeException: RuntimeException in 'some code'

fun main() {
    try {
        functionThatThrows()
    } catch (exception: Exception) {
        println("Handle $exception")
    }
}

fun functionThatThrows() {
    // some code
    throw RuntimeException("RuntimeException in regular function")
}

// Output
// Handle java.lang.RuntimeException: RuntimeException in regular function

那么在協(xié)程中又是如何處理異常呢?

在Coroutines中使用try-catch

fun main() {
    val topLevelScope = CoroutineScope(Job())
    topLevelScope.launch {
        try {
            throw RuntimeException("RuntimeException in coroutine")
        } catch (exception: Exception) {
            println("Handle $exception")
        }
    }
    Thread.sleep(100)
}

// Output
// Handle java.lang.RuntimeException: RuntimeException in coroutine

但是如果我們修改代碼為如下情況

fun main() {
    val topLevelScope = CoroutineScope(Job())
    topLevelScope.launch {
        try {
            launch {
                throw RuntimeException("RuntimeException in nested coroutine")
            }
        } catch (exception: Exception) {
            println("Handle $exception")
        }
    }
    Thread.sleep(100)
}

// Output
// Exception in thread "main" java.lang.RuntimeException: RuntimeException in nested coroutine

異常并沒有被catch住。這是因為協(xié)程自身并不能過通過try catch來捕獲異常。

協(xié)程是一種具有父子關(guān)系的Job層級結(jié)構(gòu)。

job 層級結(jié)構(gòu)
異常傳遞

如果有安裝過CoroutineExceptionHandler的話,傳遞的異常通常會被CoroutineExceptionHandler處理,如果沒有安裝過,異常將會由線程的未捕獲異常處理器處理。

總結(jié)1:如果協(xié)程內(nèi)部沒有通過try-catch處理異常,那么異常并不會被重新拋出或者被外部的try-catch捕獲。異常將會在job層級結(jié)構(gòu)中向上傳遞,將會被安裝的CoroutineExceptionHandler處理,如果沒有安裝過,異常將會被線程的未捕獲的異常處理器處理。

Coroutine Exception Handler
fun main() {
  
    val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("Handle $exception in CoroutineExceptionHandler")
    }
    
    val topLevelScope = CoroutineScope(Job())

    topLevelScope.launch {
        launch(coroutineExceptionHandler) {
            throw RuntimeException("RuntimeException in nested coroutine")
        }
    }

    Thread.sleep(100)
}

// Output
// Exception in thread "DefaultDispatcher-worker-2" java.lang.RuntimeException: RuntimeException in nested coroutine

我們?nèi)绱诵薷拇a,異常仍然沒有被捕獲。那是因為在子協(xié)程內(nèi)安裝CoroutineExceptionHandler是不會有任何效果的。
只有在作用域內(nèi)或者頂級協(xié)程內(nèi)安裝CoroutineExceptionHandler才會有效果。

// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...

或者

// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...

效果如下

// ..
// Output: 
// Handle java.lang.RuntimeException: RuntimeException in nested coroutine in CoroutineExceptionHandler

總結(jié)2:只有安裝在CoroutineScope或者頂級協(xié)程的CoroutineExceptionHandler才會生效

try-cach VS CoroutineExceptionHandler

針對如何選擇try-cach 和CoroutineExceptionHandler來處理異常,官方文檔也給出了如下建議:

  • CoroutineExceptionHandler是全局異常捕獲的最后手段,你不能在CoroutineExceptionHandler中恢復你的操作,當相應的異常處理器被調(diào)用的時候,協(xié)程已經(jīng)完成了。通常,handler是用于打印異常,顯示一些錯誤信息,終止,還有重啟應用。

  • 如果你需要在特定的代碼中處理異常,那么try-cach是推薦的異常處理方法,這可以幫助你避免因為異常而終止協(xié)程,以此繼續(xù)進行重試操作或者其他操作。

    總結(jié)3:如果你想要重試某些操作,或者在協(xié)程完成之前執(zhí)行某些操作,那么可以考慮使用try-cach。需要注意的是,在協(xié)程內(nèi)部使用了try-cach捕獲該異常之后,那么這個異常將不會再向上傳遞,也不能使用利用結(jié)構(gòu)性并發(fā)的取消函數(shù)。在i協(xié)程因異常結(jié)束需要打印異常信息的時候可以考慮使用CoroutineExceptionHandler

launch{} VS async{}

fun main() {

    val topLevelScope = CoroutineScope(SupervisorJob())

    topLevelScope.async {
        throw RuntimeException("RuntimeException in async coroutine")
    }

    Thread.sleep(100)
}

// No output

在上述例子中,并沒用打印異常。對于async來說,產(chǎn)生的異常同樣會馬上向上傳遞,只不過和launch相反的是,異常并不會被CoroutineExceptionHandler和線程的未捕獲異常處理器處理。async內(nèi)拋出的異常會被封裝在Deferred內(nèi)部,當我們在通過.await()獲取結(jié)果的時候,異常會被重新拋出。

注意:只有當async協(xié)程是頂級協(xié)程的時候,async內(nèi)的異常才會被封裝在Deferred內(nèi)部,除此以外,異常會被馬上向上傳遞,并且交給CoroutineExceptionHandler處理或者線程的未捕獲異常處理器處理,即使你沒有調(diào)用.await()方法。

fun main() {
  
    val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("Handle $exception in CoroutineExceptionHandler")
    }
    
    val topLevelScope = CoroutineScope(SupervisorJob() + coroutineExceptionHandler)
    topLevelScope.launch {
        async {
            throw RuntimeException("RuntimeException in async coroutine")
        }
    }
    Thread.sleep(100)
}

// Output
// Handle java.lang.RuntimeException: RuntimeException in async coroutine in CoroutineExceptionHandler

總結(jié)4:對于launch和async未捕獲的異常都會被馬上向上傳遞,然而,如果頂級協(xié)程是由launch啟動,那么異常將會由CoroutineExceptionHandler或者線程的未捕獲異常處理器處理,如果頂級協(xié)程由async啟動,那么異常將會被封裝進Deferred,在調(diào)用.await時候重新拋出。

coroutineScope{}的異常處理特性

在文章開頭,在try-cach內(nèi)啟動的協(xié)程內(nèi)的異常不能被捕獲,但如果在失敗的協(xié)程外部套上coroutineScope{}函數(shù),那就會不太一樣了:

fun main() {
    
  val topLevelScope = CoroutineScope(Job())
    
  topLevelScope.launch {
        try {
            coroutineScope {
                launch {
                    throw RuntimeException("RuntimeException in nested coroutine")
                }
            }
        } catch (exception: Exception) {
            println("Handle $exception in try/catch")
        }
    }

    Thread.sleep(100)
}

// Output 
// Handle java.lang.RuntimeException: RuntimeException in nested coroutine in try/catch

catch成功捕獲了異常,這是因為coroutineScope{}將失敗的子協(xié)程內(nèi)部的異常拋出,而沒有繼續(xù)向上傳遞。

總結(jié)5: coroutineScope{}會重新拋出失敗子協(xié)程內(nèi)的異常而不是將其繼續(xù)向上傳遞,這樣我就可以自己處理失敗子協(xié)程的異常了。

supervisorScope{}異常處理特性

fun main() {

    val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
        println("Handle $exception in CoroutineExceptionHandler")
    }

    val topLevelScope = CoroutineScope(Job())

    topLevelScope.launch {
        val job1 = launch {
            println("starting Coroutine 1")
        }

        supervisorScope {
            val job2 = launch(coroutineExceptionHandler) {
                println("starting Coroutine 2")
                throw RuntimeException("Exception in Coroutine 2")
            }

            val job3 = launch {
                println("starting Coroutine 3")
            }
        }
    }

    Thread.sleep(100)
}

// Output
// starting Coroutine 1
// starting Coroutine 2
// Handle java.lang.RuntimeException: Exception in Coroutine 2 in CoroutineExceptionHandler
// starting Coroutine 3

對于supervisorScope既不會重新拋出失敗的子協(xié)程的異常也不會將異常繼續(xù)向上傳遞。
對于job層級結(jié)構(gòu),異常會一直向上傳遞直到遇到頂級協(xié)程作用域或者SupervisorJob為止。

supervisorScope內(nèi)直接啟動的作為頂級協(xié)程將異常封裝進Deferred對象

// ... other code is identical to example above
supervisorScope {
    val job2 = async {
        println("starting Coroutine 2")
        throw RuntimeException("Exception in Coroutine 2")
    }
}
// ...

// Output: 
// starting Coroutine 1
// starting Coroutine 2
// starting Coroutine 3

只有在調(diào)用.await()的時候才會重新拋出異常。

總結(jié)6:作用域函數(shù)supervisorScope{}會在job層級中安裝一個獨立的新的子作用域,并使用SupervisorJob作為該作用域的job,這個新的作用域并不會將異常繼續(xù)向上傳遞,異常將由它自己處理。在supervisorScope內(nèi)直接啟動的協(xié)程將作為頂級協(xié)程。頂級協(xié)程在由launch或者async啟動的時候,它的表現(xiàn)和作為子協(xié)程時的表現(xiàn)將有所不同。

原文地址
異常傳遞流程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。