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的继承链向上传递。下面是上面代码的父子协程关系图:
CoroutineScope是topLevelScope,第一个launch是Top-Level Coroutine,第二个和更多被它包裹住的算是Child Coroutine.
要判断继承关系需要看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 Job
s 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) 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)val deferred = topLevelScope.async { throw RuntimeException("RuntimeException in nested coroutine" ) } topLevelScope2.launch { try { deferred.await() } 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 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函数 根据它的函数注释,提炼出一下要点:
Creates a CoroutineScope and calls the specified suspend block with this scope.
The provided scope inherits its coroutineContext from the outer scope, but overrides the context’s Job .
When any child coroutine in this scope fails, this scope fails and all the rest of the children are cancelled(内部block有结构化并发)
This function(is a suspend function) **returns as soon as the given block and all its children coroutines are completed.**(要注意,这是一个挂起函数,会阻塞调用它的协程,当它内部的结构化并发完成,这个函数才完成)
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)
关键要注意的是,不要把它和 CoroutineScope
、 launch
、 async
等启动顶层协程或子协程的非挂起函数搞混就行。
换言之,不要被它的名字所误导,可以用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 { 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. 内部启动的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 ) }
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 supervisorScope { val job2 = async { println("starting Coroutine 2" ) throw RuntimeException("Exception in Coroutine 2" ) }
… 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() 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 ) } 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" ) 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