1. Job的继承关系

首先来了解kotlin协程作用域的父子关系,parent-child。先来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
suspend fun main() {

val exceptionHandler1 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler1")
}
val exceptionHandler2 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler2")
}
val exceptionHandler3 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler3")
}
val topLevelScope = CoroutineScope(exceptionHandler1)
topLevelScope.launch(exceptionHandler2) {
try {
launch(exceptionHandler3) {
throw RuntimeException("RuntimeException in nested coroutine")
}
} catch (exception: Exception) {
println("Handle $exception in try/catch")
}
}

delay(5000)
}

如果一个协程报错,然后try/catch没有包裹报错的代码,而是包裹报错的那个协程,那么exception并不会被catch到,也就是协程里面的exception没有re-throw,而是propagated up,沿着job的继承链向上传递。下面是上面代码的父子协程关系图:

代码的协程继承关系

  1. CoroutineScope是topLevelScope,第一个launch是Top-Level Coroutine,第二个和更多被它包裹住的算是Child Coroutine.

  2. 要判断继承关系需要看Job的继承,而不是看launch,不然会错过。CoroutineScope和launch中都有一个默认的Job参数作为Context。

    这段话解读的很好:”To make all the features of Structured Concurrency possible, the Job object of a CoroutineScope and the Job objects of Coroutines and Child-Coroutines form a hierarchy of parent-child relationships. An uncaught exception, instead of being re-thrown, is “propagated up the job hierarchy”. This exception propagation leads to the failure of the parent Job, which in turn leads to the cancellation of all the Jobs of its children.“

2. ExceptionHandler的位置

在上面的代码例子中,

1)如果exceptionHandler1和exceptionHandler2都在,那么exception上升到exceptionHandler2就被处理了,

2)如果exceptionHandler1和exceptionHandler2只有任意一个,那么就由存在的那个处理。

3)如果exceptionHandler1和exceptionHandler2都不在,那么就会去到线程的错误处理——crash。

4)在一个child coroutine中安装exceptionHandler没有效果。

总结:In order for a CoroutineExceptionHandler to have an effect, it must be installed either in the CoroutineScope or in a top-level coroutine.

要么

1
2
3
// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...

要么

1
2
3
// ...
topLevelScope.launch(coroutineExceptionHandler) {
// ...

在上面的那个例子中,由topLevelScope launch的其他顶层协程都因为一个顶层协程的报错而被取消,当然这个cancel是合作式的,仅仅是发出取消的指令。

1
2
3
4
5
6
7
8
9
10
val topLevelScope = CoroutineScope(exceptionHandler1)
topLevelScope.launch() {
// 发生报错
}
topLevelScope.launch {// 被取消
while (true) {
delay(1000)// 合作式取消
println("循环打印")
}
}

3. async的异常处理

由async启动的协程的错误处理和launch有点不同。

1.async启动的协程是Top-Level 协程

看一段代码:

1
2
3
4
5
6
7
8
9
10
11
val exceptionHandler1 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler1")
}
val topLevelScope = CoroutineScope(exceptionHandler1)

// 顶层async
val deferred = topLevelScope.async {
// 发生报错
throw RuntimeException("RuntimeException in nested coroutine")
}

报错既不会在async处 re-throw也不会上升到handler1处理,而是会在调用await()处被re-throw。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
val exceptionHandler1 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler1")
}
val exceptionHandler2 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler2")
}
val topLevelScope = CoroutineScope(exceptionHandler1)
val topLevelScope2 = CoroutineScope(exceptionHandler2)

// 顶层async
val deferred = topLevelScope.async {
// 发生报错
throw RuntimeException("RuntimeException in nested coroutine")
}

topLevelScope2.launch {
try {
deferred.await()// re-throw here
} catch (e: Exception) {
println("Handle $e in try/catch")
}
}

打印:

1
Handle java.lang.RuntimeException: RuntimeException in nested coroutine in try/catch

总结:这种情况,异常被封装在Deferred之中,只有在调用await处才会被re-throw。只需要在await()处调用try/catch捕获。

2.async启动的协程是Child协程

看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
suspend fun main() {


val exceptionHandler1 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler1")
}
val exceptionHandler2 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler2")
}
val topLevelScope = CoroutineScope(exceptionHandler1)
val topLevelScope2 = CoroutineScope(exceptionHandler2)

var deferred2: Deferred<Nothing>? = null
// 顶层async
topLevelScope.launch {
deferred2 = async {
// 发生报错
throw RuntimeException("RuntimeException in nested coroutine")
}
}

topLevelScope2.launch {
try {
while (deferred2 == null)
print("")
delay(5000)
println("after 5 seconds")
deferred2!!.await()
} catch (e: Exception) {
println("Handle $e in try/catch")
}
}

delay(800000)
}

打印:

1
2
3
Handle java.lang.RuntimeException: RuntimeException in nested coroutine in handler1
after 5 seconds
Handle java.lang.RuntimeException: RuntimeException in nested coroutine in try/catch

可以看到,async里面的报错,立即沿着Job继承链上升——propagated up,即使没调用await(),并且也会在await()处re-throw。

如果topLevelScope的exceptionHandler1被去掉,那么会crash,因为没有handler捕获这个上升的报错。

1
2
3
4
5
6
7
8
9
10
11
Exception in thread "DefaultDispatcher-worker-3" java.lang.RuntimeException: RuntimeException in nested coroutine
at com.example.composeproject.TestKt$main$2$1.invokeSuspend(Test.kt:25)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@596502e1, Dispatchers.Default]
after 5 seconds
Handle java.lang.RuntimeException: RuntimeException in nested coroutine in try/catch

总结:抛出异常后,

1)需要在Job链添加Handler捕获

2)在await()处re-throw的异常也要捕获。

放一个表格总结一下对于Uncaught Exception(没有在协程体内被try/catch的异常)的协程处理规律:

launch async
top level Coroutine 沿着Job上升 被deferred包装,在await处re-throw
child Coroutine 沿着Job上升 立刻沿着Job上升(即使不调用await),调用await时也re-throw

4. coroutineScope函数

根据它的函数注释,提炼出一下要点:

  1. Creates a CoroutineScope and calls the specified suspend block with this scope.
  2. The provided scope inherits its coroutineContext from the outer scope, but overrides the context’s Job.
  3. When any child coroutine in this scope fails, this scope fails and all the rest of the children are cancelled(内部block有结构化并发)
  4. This function(is a suspend function) **returns as soon as the given block and all its children coroutines are completed.**(要注意,这是一个挂起函数,会阻塞调用它的协程,当它内部的结构化并发完成,这个函数才完成)
  5. The method may throw a CancellationException if the current job was cancelled externally or may throw a corresponding unhandled Throwable if there is any unhandled exception in this scope (for example, from a crashed coroutine that was started with launch in this scope)(block有异常或者block开的子协程抛异常,在这个函数调用处re-throw)

关键要注意的是,不要把它和 CoroutineScopelaunchasync 等启动顶层协程或子协程的非挂起函数搞混就行。

换言之,不要被它的名字所误导,可以用try/catch捕获它抛出的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
fun main() {

val topLevelScope = CoroutineScope(Job() + object: CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*>
get() = CoroutineExceptionHandler.Key

override fun handleException(context: CoroutineContext, exception: Throwable) {
print("handle exception in coroutineexceptionhandler")
}

})

topLevelScope.launch {
try {// 没有try/catch就在CoroutineExceptionHandler捕获
coroutineScope {
launch {
throw RuntimeException("RuntimeException in nested coroutine")
}
}

} catch (exception: Exception) {
println("Handle $exception in try/catch")
}
}

Thread.sleep(100)
}

5. supervisorScope函数

下面的代码建立了如下结构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun main() {

val topLevelScope = CoroutineScope(Job())

topLevelScope.launch {
val job1 = launch {
println("starting Coroutine 1")
}

supervisorScope {
val job2 = launch {
println("starting Coroutine 2")
}

val job3 = launch {
println("starting Coroutine 3")
}
}
}

Thread.sleep(100)
}

… will create the following job hierarchy:

性质和 coroutineScope 很类似

  1. 这是一个挂起函数,会阻塞调用它的协程,当它内部的结构化并发完成,这个函数才完成

1. 内部启动的launch/async都是 top-level coroutine.

This also means we can now install a CoroutineExceptionHandler in them that is actually called:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
fun main() {

val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}

val topLevelScope = CoroutineScope(Job())

topLevelScope.launch {
val job1 = launch {
println("starting Coroutine 1")
}

supervisorScope {
val job2 = launch(coroutineExceptionHandler) {
println("starting Coroutine 2")
throw RuntimeException("Exception in Coroutine 2")
}

val job3 = launch {
println("starting Coroutine 3")
}
}
}

Thread.sleep(100)
}

// Output
// starting Coroutine 1
// starting Coroutine 2
// Handle java.lang.RuntimeException: Exception in Coroutine 2 in CoroutineExceptionHandler
// starting Coroutine 3

The fact that coroutines that are started directly in supervisorScope are top-level Coroutines also means that async Coroutines now encapsulate their exceptions in their Deferred objects …(3.1节的情况)

1
2
3
4
5
6
7
8
9
10
11
12
13
// ... other code is identical to example above
supervisorScope {
val job2 = async {
println("starting Coroutine 2")
throw RuntimeException("Exception in Coroutine 2")
}

// ...

// Output:
// starting Coroutine 1
// starting Coroutine 2
// starting Coroutine 3

… and will only be re-thrown when calling .await()

2. supervisorScope的一些报错逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 子协程异常
fun main() {

val topLevelScope = CoroutineScope(Job())

topLevelScope.launch {
val job1 = launch {
println("starting Coroutine 1")
delay(6000)
println("Coroutine 1 end")
}

supervisorScope {
val job2 = launch {
println("starting Coroutine 2")
delay(3000)
throw IllegalStateException()// 不影响 1)Job3 2)supervisorScope块 的代码
println("Coroutine 2 end")
}

val job3 = launch {
println("starting Coroutine 3")
delay(4000)
println("Coroutine 3 end")
}
delay(5000)
println("supervisorScope end")
}
}

Thread.sleep(80000)
}

// 本身supervisorScope block异常
fun main() {

val topLevelScope = CoroutineScope(Job())

topLevelScope.launch {
val job1 = launch {
println("starting Coroutine 1")
delay(6000)
println("Coroutine 1 end")
}

try {
supervisorScope {
val job2 = launch {
println("starting Coroutine 2")
delay(3000)
println("Coroutine 2 end")
}

val job3 = launch {
println("starting Coroutine 3")
delay(4000)
println("Coroutine 3 end")
}
delay(2000)
throw IllegalStateException("supervisorScope block exception")// 1) fails the scope with all its children 2)在supervisorScope函数调用处re-throw
delay(6000)
println("supervisorScope end")
}
} catch (e: Exception) {
println("catch excception ${e.message} in try/catch")
}
}

Thread.sleep(80000)
}

总结:

内部开的协程异常 block块抛出异常
由于是顶层协程,可以给它们设置ExceptionHandler;不会在supervisorScope调用处抛出,这一点不同于coroutineScope(在它内部开的子协程抛异常会在它的调用处re-throw异常) 会在supevisorScope函数调用处re-throw

1。破坏Job链继承导致ExceptionHandler处理者改变

子协程不被父协程影响的例外情况

我之前写有文章说过这种情况。如果child coroutine的Job是显式定义一个Job(),那么它不能算是“child coroutine”了,因为它不参与它parent的结构化并发,那么在下面的代码中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
suspend fun main() {

val exceptionHandler1 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler1")
}
val exceptionHandler2 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler2")
}
val exceptionHandler3 = CoroutineExceptionHandler { coroutineContext, throwable ->
println("Handle $throwable in handler3")
}
val topLevelScope = CoroutineScope(exceptionHandler1)
topLevelScope.launch(exceptionHandler2) {
try {
launch(exceptionHandler3 + Job()) {
throw RuntimeException("RuntimeException in nested coroutine")
}
} catch (exception: Exception) {
println("Handle $exception in try/catch")
}
}

delay(5000)
}

结果将会是:

1
Handle java.lang.RuntimeException: RuntimeException in nested coroutine in handler3