[Kotlin Tutorials 22] 協程中的異常處理

協程中的異常處理

coroutine exception handling

Parent-Child關係

如果一個coroutine拋出了異常, 它將會把這個exception向上拋給它的parent, 它的parent會做以下三件事情:

  • 取消其他所有的children.
  • 取消自己.
  • 把exception繼續向上傳遞.

這是默認的異常處理關係, 取消是雙向的, child會取消parent, parent會取消所有child.

catch不住的exception

看這個代碼片段:

fun main() {
    val scope = CoroutineScope(Job())
    try {
        scope.launch {
            throw RuntimeException()
        }
    } catch (e: Exception) {
        println("Caught: $e")
    }

    Thread.sleep(100)
}

這裏的異常catch不住了.
會直接讓main函數的主進程崩掉.

這是因爲和普通的異常處理機制不同, coroutine中未被處理的異常並不是直接拋出, 而是按照job hierarchy向上傳遞給parent.

如果把try放在launch裏面還行.

默認的異常處理

默認情況下, child發生異常, parent和其他child也會被取消.

fun main() {
    println("start")
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val scope = CoroutineScope(Job() + exceptionHandler)

    scope.launch {
        println("child 1")
        delay(1000)
        println("finish child 1")
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine 1 got cancelled!")
        }
    }

    scope.launch {
        println("child 2")
        delay(100)
        println("child 2 throws exception")
        throw RuntimeException()
    }

    Thread.sleep(2000)
    println("end")
}

打印出:

start
child 1
child 2
child 2 throws exception
Coroutine 1 got cancelled!
CoroutineExceptionHandler got java.lang.RuntimeException
end

SupervisorJob

如果有一些情形, 開啓了多個child job, 但是卻不想因爲其中一個的失敗而取消其他, 怎麼辦? 用SupervisorJob.

比如:

val uiScope = CoroutineScope(SupervisorJob())

如果你用的是scope builder, 那麼用supervisorScope.

SupervisorJob改造上面的例子:

fun main() {
    println("start")
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val scope = CoroutineScope(SupervisorJob() + exceptionHandler)

    scope.launch {
        println("child 1")
        delay(1000)
        println("finish child 1")
    }.invokeOnCompletion { throwable ->
        if (throwable is CancellationException) {
            println("Coroutine 1 got cancelled!")
        }
    }

    scope.launch {
        println("child 2")
        delay(100)
        println("child 2 throws exception")
        throw RuntimeException()
    }
    Thread.sleep(2000)
    println("end")
}

輸出:

start
child 1
child 2
child 2 throws exception
CoroutineExceptionHandler got java.lang.RuntimeException
finish child 1
end

儘管coroutine 2拋出了異常, 另一個coroutine還是做完了自己的工作.

SupervisorJob的特點

SupervisorJob把取消變成了單向的, 只能從上到下傳遞, 只能parent取消child, 反之不能取消.
這樣既顧及到了由於生命週期的結束而需要的正常取消, 又避免了由於單個的child失敗而取消所有.

viewModelScope的context就是用了SupervisorJob() + Dispatchers.Main.immediate.

除了把取消變爲單向的, supervisorScope也會和coroutineScope一樣等待所有child執行結束.

supervisorScope中直接啓動的coroutine是頂級coroutine.
頂級coroutine的特性:

  • 可以加exception handler.
  • 自己處理exception.
    比如上面的例子中coroutine child 2可以直接加exception handler.

使用注意事項, SupervisorJob只有兩種寫法:

  • 作爲CoroutineScope的參數傳入: CoroutineScope(SupervisorJob()).
  • 使用supervisorScope方法.

把Job作爲coroutine builder(比如launch)的參數傳入是錯誤的做法, 不起作用, 因爲一個新的coroutine總會assign一個新的Job.

異常處理的辦法

try-catch

和普通的異常處理一樣, 我們可以用try-catch, 只是注意要在coroutine裏面:

fun main() {
    val scope = CoroutineScope(Job())
    scope.launch {
        try {
            throw RuntimeException()
        } catch (e: Exception) {
            println("Caught: $e")
        }
    }

    Thread.sleep(100)
}

這樣就能打印出:

Caught: java.lang.RuntimeException

對於launch, try要包住整塊.
對於async, try要包住await語句.

scope function: coroutineScope()

coroutineScope會把其中未處理的exception拋出來.

相比較於這段代碼中catch不到的exception:

fun main() {
    val scope = CoroutineScope(Job())
    scope.launch {
        try {
            launch {
                throw RuntimeException()
            }
        } catch (e: Exception) {
            println("Caught: $e")
        }
    }
    Thread.sleep(100)
}

沒走到catch裏, 仍然是主進程崩潰.

這個exception是可以catch到的:

fun main() {
    val scope = CoroutineScope(Job())
    scope.launch {
        try {
            coroutineScope {
                launch {
                    throw RuntimeException()
                }
            }
        } catch (e: Exception) {
            println("Caught: $e")
        }
    }

    Thread.sleep(100)
}

打印出:

Caught: java.lang.RuntimeException

因爲這裏coroutineScope把異常又重新拋出來了.

注意這裏換成supervisorScope可是不行的.

CoroutineExceptionHandler

CoroutineExceptionHandler是異常處理的最後一個機制, 此時coroutine已經結束了, 在這裏的處理通常是報告log, 展示錯誤等.
如果不加exception handler那麼unhandled exception會進一步往外拋, 如果最後都沒人處理, 那麼可能造成進程崩潰.

CoroutineExceptionHandler需要加在root coroutine上.

這是因爲child coroutines會把異常處理代理到它們的parent, 後者繼續代理到自己的parent, 一直到root.
所以對於非root的coroutine來說, 即便指定了CoroutineExceptionHandler也沒有用, 因爲異常不會傳到它.

兩個例外:

  • async的異常在Deferred對象中, CoroutineExceptionHandler也沒有任何作用.
  • supervision scope下的coroutine不會向上傳遞exception, 所以CoroutineExceptionHandler不用加在root上, 每個coroutine都可以加, 單獨處理.

通過這個例子可以看出另一個特性: CoroutineExceptionHandler只有當所有child都結束之後纔會處理異常信息.

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    val job = GlobalScope.launch(handler) {
        launch { // the first child
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(100)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
}

輸出:

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException

如果多個child都拋出異常, 只有第一個被handler處理, 其他都在exception.suppressed字段裏.

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
            } finally {
                throw ArithmeticException() // the second exception
            }
        }
        launch {
            delay(100)
            throw IOException() // the first exception
        }
        delay(Long.MAX_VALUE)
    }
    job.join()
}

輸出:

CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

單獨說一下async

async比較特殊:

  • 作爲top coroutine時, 在await的時候try-catch異常.
  • 如果是非top coroutine, async塊裏的異常會被立即拋出.

例子:

fun main() {
    val scope = CoroutineScope(SupervisorJob())
    val deferred = scope.async {
        throw RuntimeException("RuntimeException in async coroutine")
    }

    scope.launch {
        try {
            deferred.await()
        } catch (e: Exception) {
            println("Caught: $e")
        }
    }

    Thread.sleep(100)
}

這裏由於用了SupervisorJob, 所以async是top coroutine.

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)
}

當它不是top coroutine時, 異常會被直接拋出.

特殊的CancellationException

CancellationException是特殊的exception, 會被異常處理機制忽略, 即便拋出也不會向上傳遞, 所以不會取消它的parent.
但是CancellationException不能被catch, 如果它不被拋出, 其實協程沒有被成功cancel, 還會繼續執行.

CancellationException的透明特性:
如果CancellationException是由內部的其他異常引起的, 它會向上傳遞, 並且把原始的那個異常傳遞上去.

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val job = GlobalScope.launch(handler) {
        val inner = launch { // all this stack of coroutines will get cancelled
            launch {
                launch {
                    throw IOException() // the original exception
                }
            }
        }
        try {
            inner.join()
        } catch (e: CancellationException) {
            println("Rethrowing CancellationException with original cause")
            throw e // cancellation exception is rethrown, yet the original IOException gets to the handler
        }
    }
    job.join()
}

輸出:

Rethrowing CancellationException with original cause
CoroutineExceptionHandler got java.io.IOException

這裏Handler拿到的是最原始的IOException.

Further Reading

官方文檔:

Android官方文檔上鍊接的博客和視頻:

其他:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章