Kotlin的协程满足结构化语义:
A parent-Coroutine finishes only after all its child-Coroutines have finished.
When a parent-Coroutine or scope finishes abnormally, either through cancelation or through an exception, all its child-Coroutines, that are still active, are canceled and new child-Coroutines can no longer be launched.
When a child-Coroutine finishes abnormally, its parent-Coroutine or scope finishes abnormally.
针对第三点,无论是CoroutineScope.async
或CoroutineScope.launch
,以异常结束,只要是在一个scope中,他的父协程也会以异常结束。async
和launch
唯一不同的地方在于async
需要调用await才会执行协程内容。即使我们对子协程进行了try-catch处理异常,父协程仍旧会拿着这个异常作为结果结束
async的异常处理
不管是哪个启动器(launch, async等),在应用了作用域之后,都会按照**作用域的语义**进行异常扩散,进而触发相应的取消操作,对于 async 来说就算不调用 await 来获取这个异常,它也会在 coroutineScope 当中触发父协程的取消逻辑,这一点请大家注意。
以CoroutineScope(Dispatchers.Main)作为根作用域为例,下面展示几个case
1 | CoroutineScope(Dispatchers.Main).launch { |
上面的代码即使没有调用foo.await()
, 也会扩散到Thread.UncaughtExceptionHandler
中。这段代码的执行结果是
D/Foo: [, , 0]:test
同时app crash
这里我们应该这么理解,虽然是对await()
进行了try-catch,但这里是对执行结果的try-catch
, 这不影响协程自己的异常传递规则,在async中的协程scope抛出异常后,此时异常是未捕获状态;因此会向父协程scopecoroutineScope
转播, coroutineScope
继续向viewModelScope
传播,最终来到UncaughtExceptionHandler处处理
那么考虑一下直接加入try-catch
:
1 | CoroutineScope(Dispatchers.Main).launch { |
输出结果为:
E/Foo: [, , 0]:CoroutineScope caught exception: java.lang.IllegalStateException: test
D/Foo: [, , 0]:test
同时app crash
这里虽然我们对await进行了try-catch,打印了异常信息,但是根据作用域规则,这里我们的异常行为发生了扩散,从子协程扩散到根协程,最终扩散到Thread.UncaughtExceptionHandler
中,对于安卓系统而言,就是引发了crash
那么如果我们在扩散的scope外层进行try-catch能否解决问题呢?尝试如下的代码:
1 | CoroutineScope(Dispatchers.Main).launch { |
输出结果为:
E/Foo: [, , 0]:CoroutineScope caught exception: java.lang.IllegalStateException: test
D/Foo: [, , 0]:test
此时app 不再crash
如果我们在添加await()同时进行try-catch:
1 | CoroutineScope(Dispatchers.Main).launch { |
输出结果为:
E/Foo: [, , 0]:catch expection from innerScope: java.lang.IllegalStateException: test
D/Foo: [, , 0]:test
E/Foo: [, , 0]:catch expection from outerScope: java.lang.IllegalStateException: test
此时app 也不再crash,但我们会发现我们的两处try-catch
均被触发
这也说明在协程中,异常的扩散并不遵循try-catch语法构成的作用域
另一种方式就是换用一个不进行扩散语义的协程作用域,即使用supervisorScope
1 | CoroutineScope(Dispatchers.Main).launch { |
这段代码的输出结果为
D/Foo: [, , 0]:test
app 不发生crash, 同时没有IllegalStateException(“test”)的日志打印
这里的核心在于,我们的async是在supervisorScope
作用域下,根据文档描述,这个作用域下的协程出现取消情况(异常抛出时)不会向外扩散
假如加上try-catch
1 | CoroutineScope(Dispatchers.Main).launch { |
我们得到结果
E/Foo: [, , 0]:CoroutineScope caught exception: java.lang.IllegalStateException: test
D/Foo: [, , 0]:test
app 不发生crash
同理,我们的async是在supervisorScope
作用域下,根据文档描述,这个作用域下的协程出现取消情况(异常抛出时)不会向外扩散,因此不会向外扩散到Thread.UncaughtExceptionHandler
。同时,try-catch对await()发生作用,我们打印出了异常的信息
withContext的异常处理
那么对比一下withContext的处理我们就会发现不同
1 | viewModelScope.launch { |
这里withContext
直接就是一个协程scope,我们的try-catch直接作用于整个withContext构造的scope,因此异常被捕获的同时,不再向viewModelScope
传播
supervisorScope
如何能够让子协程抛出异常的情况下,父协程不会终止所以其子协程和自己呢?这就引入了supervisorScope
1 | val result = supervisorScope { |
此时,若child1或child2任意一个抛出异常,也不会使另一个child和supervising parent
停止
小结
最终我们得到了协程的Structured Concurrency含义:
A parent-Coroutine finishes only after all its child-Coroutines have finished.
When a parent-Coroutine or scope finishes abnormally, either through cancelation or through an exception, all its child-Coroutines, that are still active, are canceled and new child-Coroutines can no longer be launched.
When a child-Coroutine finishes abnormally, its parent-Coroutine or scope (a) finishes abnormally if the parent is not a supervisor or (b) keeps running if the parent is a supervisor.
P.S 协程中的异常最佳实践
通过上文中对await,withContext的异常处理的方式,我们会发现在协程中处理异常其实是一件非常麻烦的事情,其异常的扩散规则并不合try-catch
的作用域对应。因此这里简单提一下如何在实际项目中处理这种异常
Kotlin中对异常处理有两种推荐方式:
- default value
- Wrapper Seal class
第一种方式就是返回一个跟函数签名同类型的默认值,例如
1 | fun toIntSafely(defaultValue: Int) : Int { |
第二种方式则是在API设计上使用一个Wrapper Class来包装出现的异常,使用这个Wrapper Class来保证Type-Check,举例来说:
1 |
|
那么对调用者而言,tryParse
的结果一定是一个ParsedDate
对象,而对异常无感知
结合协程使用来说,我们应该直接在一个作用域内,就返回一个default value或Wrapper Seal class,例如
1 |
|
输出结果:
E/Foo: [, , 0]:catch exception inside: java.lang.IllegalStateException: test
D/Foo: [, , 0]:return rst: -1
同时app无crash,无异常日志打印
ref:
https://www.bennyhuo.com/2019/04/23/coroutine-exceptions/#4-%E5%BC%82%E5%B8%B8%E4%BC%A0%E6%92%AD
https://johnnyshieh.me/posts/kotlin-coroutine-exception-handling/
https://medium.com/@elizarov/structured-concurrency-722d765aa952
https://medium.com/the-kotlin-chronicle/coroutine-exceptions-supervision-15056802998b
https://medium.com/the-kotlin-chronicle/coroutine-exceptions-3378f51a7d33