例外處理(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