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

·  阅读 378

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 特性 单向向下传播

  • 监督作用域的传播机制 (独立决策的权利?)

示意图

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)

  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

参考资料

异常处理与监督
协程异常机制与优雅封装
协程异常到底是怎么传播的
破解 Kotlin 协程(4) - 异常处理篇
协程和异常和取消
开篇 First things first(Part 1)
协程的取消机制 Cancellation in coroutines (Part 2)
协程中的异常处理 Exceptions in coroutines-part3
协程不应被取消的设计模式 Coroutines & Patterns for work that shouldn’t be cancelled-Part 4

复制代码
分类:
Android
标签:
分类:
Android
标签: