注册

Kotlin协程的取消和异常传播机制

1.协程核心概念回顾

结构化并发(Structured Concurrency)

作用域(CoroutineScope /SupervisorScope)

作业(Job/SupervisorJob)

开启协程(launch/async)

2.协程的取消

2.1 协程的取消操作

Job生命周期

  • 作用域或作业的取消

示例代码

   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

0 个评论

要回复文章请先登录注册