Kotlin协程的取消和异常传播机制
1.协程核心概念回顾
结构化并发(Structured Concurrency)
作用域(CoroutineScope /SupervisorScope)
作业(Job/SupervisorJob)
开启协程(launch/async)
2.协程的取消
2.1 协程的取消操作
- 作用域或作业的取消
示例代码
suspend fun c01_cancle() {
val scope = CoroutineScope(Job())
val job1 = scope.launch { }
val job2 = scope.launch { }
//取消作业
job1.cancel()
job2.cancel()
//取消作用域
scope.cancel()
}
注意:不能在已取消的作用域中再开启协程
2.2确保协程可以被取消
- 协程的取消只是标记了协程的取消状态,并未真正取消协程
示例代码:
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5) {
println("Hello ${i++}")
Thread.sleep(100)
}
}
delay(200)
println("Cancel!")
job.cancel()
打印结果://未真正取消,直接检查
Hello 0
Hello 1
Hello 2
Cancel!
Hello 3
Hello 4
- 可以用 isActive ensureActive() yield来在关键位置做检查,确保协程可以正常关闭
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5 && isActive) {//方法1
ensureActive()//方法2
yield()//方法3
println("Hello ${i++}")
Thread.sleep(100)
}
}
delay(200)
println("Cancel!")
job.cancel()
2.3 协程取消后的资源关闭
- try/finally可以关闭资源
launch {
try {
openIo()//开启文件io
delay(100)
throw ArithmeticException()
} finally {
println("协程结束")
closeIo()//关闭文件io
}
}
- 注意:finally中不能调用挂起函数(如果一定要调用,需要用withContext(NonCancellable),不推荐使用)
launch {
try {
work()
} finally {
//withContext(NonCancellable)可以执行,不然不会再被执行
withContext(NonCancellable) {
delay(1000L) // 挂起方法
println("Cleanup done!")
}
}
}
2.4 CancellationException 会被忽略
val job = launch {
try {
delay(Long.MAX_VALUE)
} catch (e: Exception) {
println("捕获到一个异常$e")
//打印:捕获到一个异常java.util.concurrent.CancellationException: 我是一个取消异常
}
}
yield()
job.cancel(CancellationException("我是一个取消异常"))
job.join()
3.协程的异常传播机制
3.1 捕捉协程异常
3.1.1 try/catch
- try/catch业务代码
launch {
try {
throw ArithmeticException("计算错误")
} catch (e: Exception) {
println("捕获到一个异常$e")
}
}
//打印:捕获到一个异常java.lang.ArithmeticException: 计算错误
- try/catch协程
try {
launch {
throw ArithmeticException("计算错误")
}
} catch (e: Exception) {
println("捕获到一个异常$e")
}
//无法捕捉到 error日志
Exception in thread "main" java.lang.ArithmeticException: 计算错误
at com.jinbo.kotlin.coroutine.C05_Exception$testDemo$2$1.invokeSuspend(C05_Exception.kt:65)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.jinbo.kotlin.coroutine.C05_Exception.main(C05_Exception.kt:17)
- 无法通过外部try-catch语句来捕获协程异常
3.1.2 CoroutineExceptionHandler 捕捉异常
supervisorScope {
val exceptionHandler = CoroutineExceptionHandler { _, e ->
println("捕获到一个异常$e")
}
launch(exceptionHandler) {
throw ArithmeticException("计算错误")
}
}
//捕获到一个异常java.lang.ArithmeticException: 计算错误
3.1.3 runCatching 捕捉异常
val catching = kotlin.runCatching {
"hello"
throw ArithmeticException("我是一个异常")
}
if (catching.isSuccess) {
println("正常结果是${catching.getOrNull()}")
} else {
println("失败了,原因是:${catching.exceptionOrNull()}")
}
这时,就要介绍协程的异常传播机制
3.2 协同作用域的传播机制
3.2.1 特性
- 双向传播,取消子协程,取消自己,向父协程传播
[协同作用域传播特性]
coroutineScope {
launch {
launch {
//子协程的异常,会向上传播
throw ArithmeticException() }
}
launch {
launch { }
}
}
3.2.2 子协程无法捕获自己的异常,只有父协程才可以
val scope = CoroutineScope(Job())
//父协程(根协程)才可以捕获异常
scope.launch(exceptionHandler) {
launch {
throw ArithmeticException("我是一个子异常")
}
//这时不会捕获到,会向上传播
// launch(exceptionHandler) {
// throw ArithmeticException("我是另外一个子异常")
// }
}
3.2.3 当父协程的所有子协程都结束后,原始的异常才会被父协程处理
val handler = CoroutineExceptionHandler { _, exception ->
println("捕捉到异常: $exception")
}
val job = GlobalScope.launch(handler) {
launch { // 第一个子协程
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("第一个子协程还在运行,所以暂时不会处理异常")
delay(100)
println("现在子协程处理完成了")
}
}
}
launch { // 第二个子协程
delay(10)
println("第二个子协程出异常了")
throw ArithmeticException()
}
}
job.join()
//打印结棍:
第二个子协程出异常了
第一个子协程还在运行,所以暂时不会处理异常
现在子协程处理完成了
捕捉到异常: java.lang.ArithmeticException
3.2.4 异常聚合
第 1 个发生的异常会被优先y处理,在此之后发生的所有其他异常会被添加到最先发生的异常上, 作为被压制(suppressed)的异常
val handler = CoroutineExceptionHandler { _, exception ->
println("捕捉到异常: $exception ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
delay(100)
throw IOException() // 第一个异常
}
launch {
try {
delay(Long.MAX_VALUE) // 当另一个同级的协程因 IOException 失败时,它将被取消
} finally {
throw ArithmeticException() // 同时抛出第二个异常
}
}
delay(Long.MAX_VALUE)
}
job.join()
输出:
捕捉到异常: java.io.IOException [java.lang.ArithmeticException]
3.2.5 launch 和 async异常处理
- launch 直接抛出异常,无等待
launch {
throw ArithmeticException("launch异常")
}
//打印
Exception in thread "main" java.lang.ArithmeticException: launch异常
- async预期会在用户调用await()时,再反馈异常
直接在根协程(GlobalScope) 或 supervisor子协程时,async会在await()时抛出异常
supervisorScope {
val deferred = async {
throw ArithmeticException("异常")
}
}
//打印结果:空
- 在await()时才抛出异常
supervisorScope {
val deferred = async {
throw ArithmeticException("异常")
}
try {
deferred.await()
} catch (e: Exception) {
println("捕获到一个异常$e")
}
}
//打印结果:
捕获到一个异常java.lang.ArithmeticException: 异常
- tips: 如果不是直接在根协程(GlobalScope) 或 supervisor子协程时,async 和 launch表现一致,直接抛出异常,不会在await()时,再抛出异常
supervisorScope {
launch {
val deferred = async {
throw ArithmeticException("异常")
}
}
}
3.2.6 coroutineScope外部可以用try-catch捕获(supervisor不可以)
try {
coroutineScope {
launch {
throw ArithmeticException("异常")
}
}
} catch (e: Exception) {
println("捕捉到异常:$e")
}
//打印结果:
捕捉到异常:java.lang.ArithmeticException: 异常
3.3 监督作用域的传播机制
3.3.1 特性 单向向下传播
- 监督作用域的传播机制 (独立决策的权利?)
- supervisor的示例代码
3.3.2 子协程可以单独设置CoroutineExceptionHandler
supervisorScope {
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
打印结果:
发现了异常java.lang.ArithmeticException: 异常出现了
3.3.3 监督作业只对它直接的子协程有用
supervisorScope {
//监督作业只对它直接的子协程有用
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
-无效示例代码
supervisorScope {
launch {
//监督作业的子子协程无法独立处理异常,向上抛异常
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
}
//打印结果:
Exception in thread "main" java.lang.ArithmeticException: 异常出现了
at com.jinbo.kotlin.coroutine.C05_Exception$testDemo$2$1$1$1.invokeSuspend(C05_Exception.kt:1039)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.jinbo.kotlin.coroutine.C05_Exception.main(C05_Exception.kt:17)
3.4 正确使用coroutineExceptionHandler
3.4.1 根协程(GlobalScope)//TODO 确认
GlobalScope.launch(exceptionHandler) { }
3.4.2 supervisorScope 直接子级
supervisorScope {
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
3.4.3 手动创建的Scope(Job()/SupervisorJob())
val scope = CoroutineScope(Job())
scope.launch(exceptionHandler) {
throw ArithmeticException("异常")
}
4 思考
4.1 android 的协同
- viewmodelScope lifecycleScope