iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Software Development

Coroutine 停看聽系列 第 10

Day10:例外處理,留下來或我跟你走

程式在執行的時候,有些時候我們會遇到一些例外的情況,我們一般會使用 try-catch 來攔截程式執行所拋出的例外,用 try-catch 攔截到之後,我們就可以視情況要自己處理還是要再把這個例外轉拋出去。(或是不處理,就讓系統崩潰)

假設有一個函式,執行之後會有可能會拋出 RuntimeException 。所以當程式執行到這個函式的時候,就必須面臨處理它的問題。

fun throwException() {
    throw RuntimeException("Incorrect")
}

不處理

fun main(){
	throwException()
}

→ 系統崩潰並收到一個錯誤訊息。(Exception in thread "main" java.lang.RuntimeException: Incorrect)

處理

fun main(){
	try {
      throwException()
    } catch (e: RuntimeException) {
      println(e.message)
	  }
}

→ 在 try-catch 中攔截到這個例外,所以我們可以針對發生這個例外的情況來做處理。(如上方把錯誤資訊列印出來)


如果在 Coroutine 發生例外,會是怎麽的處理方式呢?

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

→ 這段程式使用 launch 建立了兩個子 Coroutine ,一個 launch 執行一段很長時間的延遲,另一個則是延遲 100 毫秒之後,就拋出 RuntimeException()

執行這段程式碼試試看:

RuntimeException

流程如下,第二個 coroutine 拋出 RuntimeException 後,父 coroutine 的 Job 就把剩下的所有子 coroutine 取消。所以當例外發生的時候,後面還沒有執行完成的 coroutine 就會被取消。

在呼叫有可能會發生例外的函式時,使用 try-catch 把例外攔住

如果我們本來就知道哪一個函式會發生例外,我們可以直接使用 try-catch,把例外自行處理掉,就不會傳到父 coroutine 來處理了。

將前面的範例改成:

launch {
    delay(100)
    println("Second child throws an exception")
		try{
		    throw RuntimeException()
		}catch(e: RuntimeException){
			println("Catch exception")
		}
}

try-catch exception

→ 第一個 coroutine 不會因為第二個 coroutine 發生例外而被取消。

CoroutineExceptionHandler

在建立 Coroutine 的時候,我們可以建立 CoroutineContext.Element 帶入,其中有一個 Element 就是用來做例外處理的。

其名稱為 CoroutineExceptionHandler

要如何使用呢?

將上方程式改為:

class Day10 {
		private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }

    private val scope = CoroutineScope(coroutineExceptionHandler)

		suspend fun launchWithException(){
				val job = scope.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()
		}
}

→ 我們使用 CoroutineExceptionHandler 這個方法來建立 CoroutineExceptionHandler 的實例。

這邊聽起來很饒舌,在 Coroutine 中,有一個介面名為 CoroutineExceptionHandler ,它是繼承 CoroutineContext.Element ,所以我們可以實作它並傳進 Coroutine Context 中。

另外, Coroutine 也同時提供了一個函式,用來建立這個介面的實例,而這個函式的名稱也叫做 CoroutineExceptionHandler。

這個方法實作如下:

public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineContext, Throwable) -> Unit): CoroutineExceptionHandler =
    object : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
        override fun handleException(context: CoroutineContext, exception: Throwable) =
            handler.invoke(context, exception)
    }

發生例外時就會調用 handleException,把 Coroutine context 以及 exception 傳出去。

上面的程式改寫完後,我們可以測試一下:

fun main() = runBlocking{
    val day10 = Day10()
    day10.launchWithException()
}

coroutineExceptionHandler

當我們的 Coroutine Scope 中有包含 CoroutineExceptionHandler 時,所有未經處理的例外都會傳到這邊,我們就可以在這個地方去做處理。


async

上面的範例是使用 launch 來示範的,如果是 async ,我們也可以使用 CoroutineExceptionHandler 來攔截例外嗎?

  • async with exception
suspend fun asyncException(): Int {
    val deferred1 = scope.async {
        delay(100)
        10
    }
    val deferred2 = scope.async<Int> {
        delay(200)
        20
        throw RuntimeException("Incorrect")
    }
    return deferred1.await() + deferred2.await()
}

→ 我們有一個函式,在這裡面我們用 async 建立了兩個 coroutine ,在第一個 Coroutine 中,我們延遲了 100 毫秒,並且回傳整數10,而另外一個 coroutine ,我們延遲了 200 毫秒,但是在最後發生了 RuntimeException

→ 這個函式的結果需要將兩個 async 的結果相加傳出去。

好的,我們把這段程式碼執行看看。

fun main() = runBlocking{
    val day10 = Day10()
    val result = day10.asyncException()
    println($result)
}

async exception

CoroutineExcaptionHandler 居然沒有把這個例外給攔截下來。

在 Coroutine 中,只有 launch 以及 actor 裏面的例外能夠被 CoroutineExceptionHandler 給攔截, async 以及 produce 的例外則是會往外拋給使用者。

那我們該如何處理 async 的例外呢?

在這邊我們可以使用 try-catch 來攔截,我們把上面的範例程式用 try-catch 包起來

fun main() = runBlocking{
    val day10 = Day10()
		try {
        val result = day10.asyncException()
        println("$result")
    } catch (e: RuntimeException) {
        println("${e.message}")
    }
}

try-catch async

沒錯,用 try-catch 就可以把 async 的結果攔截下來了。

小結

如果 Coroutine 的 job 為 Job(),在 Coroutine 一層一層的架構下,只要有一個 coroutine 發生例外就會導致其他的子 coroutine 被取消,如果想要避免這個情況,可以在可能發生例外的地方加上 try-catch 來作保護,讓程式不會因為例外而取消所有的 coroutine。

假如我們沒有把例外攔截下來,最後就會傳到父 coroutine 的 CoroutineExceptionHandler (如果有設定的話)。

另外,launch 與 async 處理例外的方式各有不同, launch 是會往前傳直到父 coroutine 的 CoroutineExceptionHandler,async 是把例外直接傳給呼叫的地方,故我們需要在呼叫的地方使用 try-catch 攔截。

參考資料

Exceptions in coroutines

Exception handling


上一篇
Day9:Job vs SupervisorJob
下一篇
Day11:調度器(Dispatchers),我跳進來了,又跳出去了
系列文
Coroutine 停看聽30

尚未有邦友留言

立即登入留言