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


<<:  案例:MLOps在医疗产业(上) - 5个常见案例与3个风险来源

>>:  [Day14] Esp32s用STA mode + LED - (程序码讲解)

Day22 ( 高级 ) 猫咪万花筒

猫咪万花筒 教学原文参考:猫咪万花筒 这篇文章会介绍,在 Scratch 3 里使用扩充功能的画笔,...

Day 01:程序设计师的一天

前言 这是一个七年 Android 工程师专为麻瓜写的。 麻瓜是指: 不会魔法、选错科系入错行、被老...

[DAY03] 建立 Datastore 和 Dataset (上)

DAY03 建立 Datastore 和 Dataset (上) 我们都知道做 AI 最重要的就是 ...

Day 25 实作 user_bp (3)

前言 我们今天还是没有离开 user_bp,我们要来弄写文章的页面,也就是 markdown 上场的...

Day 8:先别急着撰写文章,你听过 Markdown 吗?

相信有人已经迫不及待要撰写文章了,不过在这之前,我们先来介绍一下 Markdown 这个标记语言。 ...