kotlin协程四

前言

翻译自 协程异常
这一章节将介绍在协程中异常是如何传播的,以及如何通过不同的方法处理他们。

协程突然失败了,怎么办

如果一个协程exception了,会将上述异常传给它的父级。之后,父级会1.取消其他的子级2. 取消自己3.传递异常给他的父级
这个异常会传达到根级,所有这个scope开始的协程都会被取消。



尽管在某些情况下传播异常可能很有意义,但也有一些不期望传异常的。想象一个专门处理用户操作的与UI有关的scope。如果一个子协程抛出异常,UI scope也会被取消,整个UI 组件都会没有反应,因为一个取消掉的scope不能再开始更多的协程.
那该怎么办呢?或者,可以在创建这些协程的CoroutineScope的CoroutineContext中使用Job的不同实现,即SupervisorJob。

SupervisorJob拯救

使用SupervisorJob ,child的失败将不会影响其他的child。SupervisorJob不会取消自己或者其他的子级。而且,SupervisorJob不会传递异常,并且允许child去处理异常。
你可以用类似val uiScope = CoroutineScope(SupervisorJob())创建一个scope,这样当一个协程失败了,也不会去传递cancellation.


如果这个异常没有处理,并且CoroutineContext没有CoroutineExceptionHandler(后续会讲),它将会传到默认的线程ExceptionHandler。在虚拟机中,这个异常会被打印在console中,在安卓中,它会让你的APP crash,无论是在哪个dispatcher上发生的。
note: 没有被拦截的异常总会被抛出,不管你用的哪种job。
同样的行为适用于coroutinescope和supervisorscope。这些将创建一个子scope(相应的job或SupervisorJob作为父级),这样你可以有逻辑的组织协程。(例如。做并行的计算或者让他们互相影响或互不影响)
注意:仅当supervisorJob作为scope的一部分才可以像描述的那样:使用supervisorScope或CoroutineScope(SupervisorJob())创建

Job还是SupervisorJob?

用SupervisorJob 或者supervisorScope当你不想要失败的时候去取消父级和同级。
例如

val scope = CoroutineScope(SupervisorJob())
scope.launch {
    // Child 1
}
scope.launch {
    // Child 2
}

在这种情况下,如果child1失败了,不会取消child2和scope。
例如,

val scope = CoroutineScope(Job())
scope.launch {
    supervisorScope {
        launch {
            // Child 1
        }
        launch {
            // Child 2
        }
    }
}

这个例子,supervisorScope用supervisorJob创建了一个子scope,如果child1失败了,child2不会被取消。
相反,如果使用coroutineScope,失败将会传递并且也会取消掉scope。

谁是我的父级?

你能辨别出下面的代码中谁是child1的父级吗?

val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
    // new coroutine -> can suspend
   launch {
        // Child 1
    }
    launch {
        // Child 2
    }
}

child1的父级是job类型,即使第一印象你觉得是supervisorjob类型,但那是不对的。不是因为一个新的协程总会创建一个新的job,而是因为在这种情况下,会覆盖掉supervisorjob。supervisorjob是scope.launch创建的协程的父级,SupervisorJob在这段代码里啥也没干,也就是没啥用。


child1和child2的父级都是job类型,不是supervisorjob

因此,无论child1.还是child2失败,这个失败都会到达scope,所有被这个scope开始的工作都会被cancel掉。
记住supervisorjob只有在使用supervisorScope或者CoroutineScope(SupervisorJob())才有用 。作为创建协程的参数的supervisorJob并不会有你期待的cancellation作用。
对于异常,如果一个子级抛出异常,supervisorjob不会传递异常,并允许自己的协程去处理它。

幕后工作

如果你好奇job的幕后工作感到好奇,可以查看JobSupport.kt文件的childCancelled和notifycabcelling方法。
在SupervisorJob的实现类中,childCancelled返回了false,这意味着它不会传递取消但也不处理异常。

异常处理

协程使用常规的Kotlin语法来处理异常:try / catch或内置帮助程序功能(如runCatching(内部使用try / catch))
我们之前说过总会抛出不被捕获的异常。然而,不同协程构建器用不同的方式处理异常。

Launch

异常会在发生的时候立刻抛出。因此,可以这样子

scope.launch {
    try {
        codeThatCanThrowExceptions()
    } catch(e: Exception) {
        // Handle exception
    }
}
Async

当async用作根协程时(是CoroutineScope实例或supervisorScope的直接子级),异常不会立刻抛出,而是在调用.await()时抛出。例如

supervisorScope {
    val deferred = async {
        codeThatCanThrowExceptions()
    }
    try {
        deferred.await()
    } catch(e: Exception) {
        // Handle exception thrown in async
    }
}

在这个示例中,注意调用async永远不会抛出异常,await才会抛出async协程的异常。
注意,我们使用的是supervisorScope去调用async和await。就像我们之前说的,supervisorjob允许协程去处理异常。相反的,如果用job将会自动将异常向上传递,因此catch块无用。

coroutineScope {
    try {
        val deferred = async {
            codeThatCanThrowExceptions()
        }
        deferred.await()
    } catch(e: Exception) {
        // 异常永远不会被这里捕获,而是会向上传给scope
    }
}

此外,通过其他协程创建的协程发生异常,一定会被传递,无论使用的哪种构造器。例如

val scope = CoroutineScope(Job())
scope.launch {
    async {
        // 如果async抛出异常,launch会抛出而不需要调用.await()
    }
}

在这个例子中,如果async会在异常发生的时候立刻抛出异常,因为协程是scope launch的直接子级。因为async(job在它自己的CoroutineContext中)会自动的将异常传给它的父级(launch)来抛出异常。
注意:在CoroutineScope构造器中或通过其他协程创建的协程抛出的异常都不能被try/catch捕获
在SupervisorJob小节中,我们提到了CoroutineExceptionHandler的存在。接下来让我们深入研究它。

CoroutineExceptionHandler

CoroutineExceptionHandler是CoroutineContext的一种可选择元素,允许你处理未捕获的异常。
下面是定义CoroutineExceptionHandler的示例,无论何时捕获到一个异常,你都会有关于CoroutineContext和异常本身的相关信息。

val handler = CoroutineExceptionHandler {
    context, exception -> println("Caught $exception")
}

如果满足一下要求,exception会被捕获:
when: 协程会自动抛出异常(launch,而非async)
where: 在CououtineScope的CoroutineContext中或根协程(CoroutineScope或supervisorScope的直接子级)
在下面这个例子中,exception会被handler捕获

val scope = CoroutineScope(Job())
scope.launch(handler) {
    launch {
        throw Exception("Failed coroutine")
    }
}

而在下面这个例子中,exception不会被捕获

val scope = CoroutineScope(Job())
scope.launch {
    launch(handler) {
        throw Exception("Failed coroutine")
    }
}

因为这个handler没有被放置在正确的CoroutineContext中。内置的launch会立刻传递异常给父级,父级不会知道关于这个handler一无所知,异常会被抛出。

优雅的处理异常是很重要的,记住用SupervisorJob当你想在异常发生时避免传递cancellation,否则用job。

后记

下一节学习不应该被取消的工作

你可能感兴趣的:(kotlin协程四)