例外處理(Exception Handling)是寫程式繞不開的議題。一般同步程式我們已經很熟悉 try-catch 的處理方式:要嘛攔下來自己處理、要嘛重新拋出,或者乾脆讓系統崩潰。Coroutine 中的取消(Cancellation)與例外處理也建立在同樣的概念上,但加上了結構化併發帶來的層級行為。本章先把焦點放在「取消單一任務」這個最基本的場景。
一般函式的例外處理#
假設有一個會拋例外的函式:
fun throwException() {
throw RuntimeException("Incorrect")
}不處理的話,整個程式會崩潰:
fun main() {
throwException()
}執行結果是 Exception in thread "main" java.lang.RuntimeException: Incorrect。
用 try-catch 攔下來就能自行處理:
fun main() {
try {
throwException()
} catch (e: RuntimeException) {
println(e.message)
}
}Coroutine 中發生例外會怎樣#
把同樣的概念搬到 Coroutine 裡。下面這段程式建立了一個父 Coroutine,裡面再開兩個子 Coroutine:第一個 launch 用 delay(Long.MAX_VALUE) 等很久,第二個 launch 在 100 毫秒後拋出 RuntimeException。
fun launchExceptionFun1() {
val job = launch {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("First children are cancelled")
}
}
launch {
delay(100)
println("Second child throws an exception")
throw RuntimeException()
}
}
job.join()
}執行流程:
- 第二個子 Coroutine 拋出
RuntimeException。 - 父 Coroutine 的 Job 把所有還沒完成的子 Coroutine 取消。
- 第一個子 Coroutine 因為被取消,會走到
finally區塊,印出First children are cancelled。
換句話說,當 Job 管理的某個子 Coroutine 出錯,例外會往父 Coroutine 傳,父 Coroutine 接著把同層其他兄弟也取消。
用 try-catch 守住可預期的例外#
如果我們知道某段程式會出錯,最直覺的做法就是在源頭把它擋下來,不要讓例外傳到父 Coroutine。把第二個 launch 改成:
launch {
delay(100)
println("Second child throws an exception")
try {
throw RuntimeException()
} catch (e: RuntimeException) {
println("Catch exception")
}
}例外被當下吃掉之後:
- 父 Coroutine 沒收到例外,不會啟動取消流程。
- 第一個 launch 也就不會被連帶取消。
這是「取消單一任務」最單純的處理方式:把例外攔在當前 Coroutine 內、自行決定要怎麼處理,後續的兄弟 Coroutine 完全不受影響。
取消透過 CancellationException 傳遞。Coroutine 框架在執行 suspend 函式時會檢查取消狀態,這也是為什麼
delay在被取消時會立刻回應,而長時間運算如果沒有檢查 isActive、ensureActive 或 yield,是不會自動退出的。詳細的協作式取消(Cooperative Cancellation)會在後續章節再深入。
小結#
- 不管在不在 Coroutine 裡,try-catch 都是處理可預期例外的第一道防線。
- 在 Coroutine 中,未被攔截的例外會傳給父 Coroutine,由 Job 把同層的兄弟一併取消。
- 想讓單一任務的例外不影響其他兄弟,最簡單的方式就是在當前 Coroutine 內用 try-catch 把它處理掉。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10264460