Kotlin协程异常传递机制

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin协程异常传递机制相关的知识,希望对你有一定的参考价值。

先说结论:

  1. 异常传播机制的前提条件是建立一棵Job树

  2. 当发生异常时,当前Job开始沿着树向上逐级询问父Job,它是否要处理

  3. 父Job B如果要处理,就没子Job A什么事了。此时,Job B它自己就成了子Job B,继续去问它的父Job C,一路问上去,直接找到一个愿意处理异常的Job。而所谓的愿意愿意处理的Job, 其实就是到达顶层Job, 它要是不处理, 那就没人能处理了, 不论怎样就它了.

  4. 父Job不愿意处理, 那就只好由子Job自己处理. 所谓的父Job不愿意处理, 有这几种情况:

    (1) 异常类型是CancellationException, 你取消就取消了呗, 多大点事, 你自己收尾

    (2) 父Job是SupervisorJob, 他会重写一个方法: childCancelled(ex) = false, 它告诉子Job, 不管是什么异常, 我都不管, 你自己处理.

  5. 不管异常是上报给父Job处理还是留着自己处理, 反正最后一定会有一个Job负责处理, 它会通过JobNode.CompletionHandler向下取消自己的所有子Job.

异常处理的入口

这个流程基本上是先看了一堆别人的博客, 然后自己边看边猜的

AbstractCoroutine.resumeWith(Result<T>) ->
JobSupport.makeCompletingOnce(result.toState()) ->
          .tryMakeCompleting(...) ->
          .tryMakeCompletingSlowPath() ->
          .finalizeFinishingState() ->

// 最终的核心方法

(1) val handled = cancelParent(finalException) || handleJobException(finalException)
cancelParent 沿树向上找到一个愿意处理此异常的Job对象
handleJobException 在没有其他Job愿意处理时, 就只能自己处理

(2) completeStateFinalization() -> (state as CompletionHandler).invoke(cause)
把自己的所有子Job也都结束掉

cancelParent 沿树向上找到一个愿意处理此异常的Job对象

先假设没有SupervisorJob, 一路上全是Job, 而且也不是取消

private fun cancelParent(cause: Throwable): Boolean 
    ...
    // 假设不是取消
    val isCancellation = cause is CancellationException
    val parent = parentHandle
    // No parent -- ignore CE, report other exceptions.
    if (parent === null || parent === NonDisposableHandle) 
        return isCancellation = false
    
    return parent.childCancelled(cause) || isCancellation = false

进入 parent.childCancelled(cause)

parent对象在构建Job树时有提到, 在说到SupervisorJob时需要再强调一遍, 此处沿着Job树往上找就可以了.

public open fun childCancelled(cause: Throwable): Boolean 
    if (cause is CancellationException) return true // 假设不是取消
    return cancelImpl(cause) && handlesException

此时进入parent对象中, 需要再次强调一遍, 面向对象的树状结构, 要时刻注意当前是在哪个对象上.

cancelImpl()中的代码看不懂, 有一大堆的state, 只能凭感觉猜测 ->

makeCancelling(cause) ->
tryMakeCompleting(...) ->
tryMakeCompletingSlowPath() ->
finalizeFinishingState() ->

最终又回到了

cancelParent(finalException) || handleJobException(finalException)

然后就这样一层一层得向上找, 直到尽头:

if (parent === null || parent === NonDisposableHandle) 
    return isCancellation 假设不是取消, return false

这样就会进入 handleJobException(finalException), 由自己处理异常

上述过程中一直在强调, 假设它不是取消,

如果是取消, 要就有这几种情形:

  1. 一路向上找到顶, return isCancellation = true,

  2. 中间任意一个父Job说它不处理, return parent.childCancelled(cause) = false || isCancellation = true, 最终返回true

  3. 先提一句SupervisorJob, 它会重写 childCancelled(ex) = false 表示自己不处理, return parent.childCancelled(cause) = false || isCancellation = true, 最终返回true

然后它就不会进入 handleJobException(finalException)

SupervisorJob时需要再强调一遍的parent

scope.launch(SupervisorJob()) 
    val newContext = 把scope.context的Job替换成参数SupervisorJob
    val job = new StandaloneCoroutine(parentContext=newContext)
      --> job.parentJob = parentContext[Job]=参数SupervisorJob
    job.start(协程体)
    return job

任务Job发生异常, 去询问父Job是否要处理, 父Job是SupervisorJob, 它不处理.

这里潜藏的父子关系比较难找出来.

handleJobException(ex) 由自己去处理异常

public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) 
    // Invoke an exception handler from the context if present
    try 
        context[CoroutineExceptionHandler]?.let 
            it.handleException(context, exception)
            return
        
     catch (t: Throwable) 
        handleCoroutineExceptionImpl(context, handlerException(exception, t))
        return
    
    // If a handler is not present in the context or an exception was thrown, fallback to the global handler
    handleCoroutineExceptionImpl(context, exception)


fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) 
    // use thread's handler
    val currentThread = Thread.currentThread()
    currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)

最终会调用到这里,哪个Job想处理这个异常,那就看看这个Job有没有设置CoroutineExceptionHandler, 如果有, 那就由它来兜底处理.

如果没有设置, 最终会执行 thread.uncaughtExceptionHandler.uncaughtException(), 而它没办法用try-catch捕获。

coroutineScope 和 supervisorScope

这一条就凭感觉硬往前面的结论上凑的, 以后如果能非常清晰得搞清楚, 再来修改.

在看字节小站的协程异常处理机制时,关于他的第7 第8点,按照前面分析代码的结论,似乎走不通,直到我尝试在第7条的try-catch里打印了错误堆栈:

2022-01-16 19:30:24.437 16178-16235/com.innocent.coroutine E/Innocent: caught : 测试
    java.lang.RuntimeException: 测试
        at com.innocent.coroutine.MainActivity$onCreate$1$1$1.invokeSuspend(MainActivity.kt:45)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

我恍然大悟, coroutineScope 不能把它理解为一个Scope对象, 而是应该理解为一个 subScope.launch(Job()) , 这样再按照上述结论, 就可以跑通了.

subScope.launch(Job()) 
    发生异常

subScope内部发生异常, 最终一路上报给subScope, 此时协程异常退出, 但因为它是sub, 跟launch很类似, 最终还是要通过 resumeWith()恢复主协程, 就在恢复主协程时, 内部异常就作为 resumeWith(Result.failure(ex)) 被捕获.

subScope.launch(SupervisorJob()) 
    发生异常

同样的, supervisorScope也可以理解为 subScope.launch(SupervisorJob()) , 内部异常因为 SupervisorJob , 交给子Job处理, 如果此子Job有innerExceptionHandler, 那就可以由innerExceptionHandler处理. 假设它没有, subScope会thread.uncaughtExceptionHandler.uncaughtException(), 因为它是sub, 所以由主协程最后兜底, 如有outterExceptionHandler则最终捕获, 如无则继续崩溃.

对这俩东西实在搞太不懂, 以后工作中我就尽量少用, 尽量用scope.launch方式

以上是关于Kotlin协程异常传递机制的主要内容,如果未能解决你的问题,请参考以下文章

kotlin协程硬核解读(5. Java异常本质&协程异常传播取消和异常处理机制)

kotlin协程硬核解读(5. Java异常本质&协程异常传播取消和异常处理机制)

kotlin协程硬核解读(5. Java异常本质&协程异常传播取消和异常处理机制)

抽丝剥茧聊Kotlin协程之协程异常处理机制分析

Kotlin 协程协程异常处理 ② ( SupervisorJob 协程 | supervisorScope 协程作用域构建器函数 )

Kotlin 协程协程异常处理 ② ( SupervisorJob 协程 | supervisorScope 协程作用域构建器函数 )