\结构化并发原理解析

前言

这篇文章来介绍一下协程结构化并发的原理。

正文

在开始之前,我们可以回忆一下,协程的结构化并发就是带有层级和结构的并发,而这个层次和结构,在前面有篇文章:

这里有介绍,简单来说就是协程存在父子关系,而这里父子关系我们使用协程句柄Job来表示的,即父Job会有个childJob变量来保存其子Job,这样就可以做到当父Job取消,子Job也取消的结构化取消效果。

在协程框架的中层概念中,CoroutineScope就是实现结构化并发的关键,其实从字面意思也非常好理解,协程作用域,也就是规定了一个作用域,可以批量管理一个作用域内的所有协程。

为什么有CoroutineScope

其实越是到后面,越容易串起来整个协程框架的知识,让知识形成体系。我们这里回顾一下启动协程的2个API:launch和async,下面是源码:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ...
}

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    ...
}

这里发现它们都是CoroutineScope的扩展函数,这里为什么要设计为CoroutineScope的扩展函数呢,如果不设计为这样,难道就不能实现结构化关系了吗

其实不然,在早期的协程API,这2个函数还真不是CoroutineScope的扩展函数,假如是使用早期API,伪代码如下:

// 使用协程最初的API,只是伪代码
private fun testScopeJob() {
    val job = Job()
    launch(job){
        launch {
            delay(1000000L)
            logX("Inner")
        }
        logX("Hello!")
        delay(1000000L)
        logX("World!")  // 不会执行
    }

    launch(job){
        launch {
            delay(1000000L)
            logX("Inner!!!")
        }
        logX("Hello!!!")
        delay(1000000L)
        logX("World1!!!")  // 不会执行
    }
    Thread.sleep(500L)
    job.cancel()
}

这里想实现结构化并发,我们不得不创建一个Job对象,然后传入launch中当做参数,但是开发者可能会忘记传输这个参数,所以就会打破结构化关系。

所以后面发展就专门设计出CoroutineScope来管理协程批量处理,而且把launch和async都作为该类的扩展函数,这样就不会有前面所说的忘记传递参数从而导致的非结构关系。

原理分析

从前面知识以及API的迭代我们知道,协程内部真正控制协程的还是Job,这个CoroutineScope只是辅助作用,我们先来看一下前面所说的协程父子关系是如何创建的。

创建关系

这里我们写出下面实例代码:

private fun testScope() {
    val scope = CoroutineScope(Job())
    scope.launch{
        launch {
            delay(1000000L)
            logX("Inner")  // 不会执行
        }
        logX("Hello!")
        delay(1000000L)
        logX("World!")  // 不会执行
    }

    Thread.sleep(500L)
    // 2
    scope.cancel()
}

这里我们创建了一个scope,这里我们来看一下这个方法源码:

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

可以发现CoroutineScope()是一个顶层函数,同理函数体内部的Job()也是一个顶层函数,这里还有一个小知识点:当顶层函数当做"构造函数"来使用时,这个函数的命名可以不使用驼峰命名法,而是以大写开始

这里返回的是CoroutineScope,在前面文章我们知道它是对CoroutineContext的封装:

public interface CoroutineScope 
    public val coroutineContext: CoroutineContext
}

internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}

这里通过context[Job]就可以取出保存在context中的Job对象,假如没有Job对象的话,就创建一个Job对象传入到context中,这说明一件事:每一个CoroutineScope对象,它的context当中必然存在一个Job对象

接着我们继续看launch的源码:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    //注释1 
    val newContext = newCoroutineContext(context)
    //注释2
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    //注释3
    coroutine.start(start, coroutine, block)
    return coroutine
}

注释1的代码我们在前面文章有说过,其实就是为新协程创建Context,而创建新Context的方式也非常简单,就是把当前scope的context和参数传递的context相加即可。

注释3的代码是协程启动,这个我们在之前文章也说过了,这里就剩注释2没有说了,而这就是本章内容的关键。

注释2这里涉及的2个类分别是:

private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

private class LazyStandaloneCoroutine(
    parentContext: CoroutineContext,
    block: suspend CoroutineScope.() -> Unit
) : StandaloneCoroutine(parentContext, active = false) {
    private val continuation = block.createCoroutineUnintercepted(this, this)

    override fun onStart() {
        continuation.startCoroutineCancellable(this)
    }
}

可以发现StandaloneCoroutine是AbstractCoroutine的子类,在前面文章中我们说过这个可以看成是代表协程的抽象类,在调用其构造函数时,第二个参数initParentJob参数,一直为true,其实就是代表了协程创建以后,需要初始化协程的父子关系。

构造函数如下:

public abstract class AbstractCoroutine<in T>(
    parentContext: CoroutineContext,
    initParentJob: Boolean,
    active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {

    init {
        if (initParentJob) initParentJob(parentContext[Job])
    }
 }

可以发现AbstractCoroutine其实就是JobSupport的子类,而这里initParentJob参数我们从前面可知,这里必为true,即初始化父子关系,其中initParentJob()函数定义在JobSupport类中:

protected fun initParentJob(parent: Job?) {
    assert { parentHandle == null }
    //注释1
    if (parent == null) {
        parentHandle = NonDisposableHandle
        return
    }
    //注释2
    parent.start()
    //注释3
    val handle = parent.attachChild(this)
    parentHandle = handle
    // now check our state _after_ registering (see tryFinalizeSimpleState order of actions)
    if (isCompleted) {
        handle.dispose()
        parentHandle = NonDisposableHandle // release it just in case, to aid GC
    }
}

这里我们来简单分析一下:

  • 注释1出判断如果parent为空,则不存在父子关系了,就没必要创建协程父子关系了。
  • 注释2确保parent对应的Job启动了。
  • 注释3就是会把当前Job给添加到parentJob的子Job中,而通过这个操作后,协程的父子关系就创建了

所以我们可以把协程看成一颗N叉树,每一个协程都对应一个Job对象,而每一个Job可以有一个父Job和多个多个子Job。

image.png

结构化取消

既然Job的关系如上图中的N叉树,所以结构化取消其实也就是事件传递了,当某个Job收到取消事件时,需要通知其上下级。

我们可以想象出其取消协程的代码应该如下:

fun Job.cancelJob() {
    // 通知子Job
    children.forEach {
        cancelJob()
    }
    // 通知父Job
    notifyParentCancel()
}

当然这是只是简化的伪代码,真实代码复杂很多,但是原理差不多。

我们先来看一下Scope的cancel代码:

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

这里会调用job的cancel()方法,而这个方法实现是在JobSupport类当中:

//外部带原因的取消,内部不能隐式调用
public override fun cancel(cause: CancellationException?) {
    cancelInternal(cause ?: defaultCancellationException())
}

public open fun cancelInternal(cause: Throwable) {
    cancelImpl(cause)
}

internal fun cancelImpl(cause: Any?): Boolean {
    var finalState: Any? = COMPLETING_ALREADY
    if (onCancelComplete) {
        //1
        finalState = cancelMakeCompleting(cause)
        if (finalState === COMPLETING_WAITING_CHILDREN) return true
    }
    if (finalState === COMPLETING_ALREADY) {
        //2
        finalState = makeCancelling(cause)
    }
    return when {
        finalState === COMPLETING_ALREADY -> true
        finalState === COMPLETING_WAITING_CHILDREN -> true
        finalState === TOO_LATE_TO_CANCEL -> false
        else -> {
            afterCompletion(finalState)
            true
        }
    }
}

这里job.cancel()最终会调用cancelImpl方法,这里有个onCancelComplete属性,其实代表当前的Job是否有协程体需要执行,我们在前面实例代码中的Job是手动创建的,所以不需要执行任何协程代码,所以会走到注释1部分代码:

继续分析:

private fun cancelMakeCompleting(cause: Any?): Any? {
    loopOnState { state ->
        // 省略部分
        val finalState = tryMakeCompleting(state, proposedUpdate)
        if (finalState !== COMPLETING_RETRY) return finalState
    }
}

private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?): Any? {
    if (state !is Incomplete)
        return COMPLETING_ALREADY

        // 省略部分
        return COMPLETING_RETRY
    }

    return tryMakeCompletingSlowPath(state, proposedUpdate)
}

private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?): Any? {
    // 省略部分
    notifyRootCause?.let { notifyCancelling(list, it) }

    return finalizeFinishingState(finishing, proposedUpdate)
}

这里代码调用比较复杂,我们可以不用关注,最终会调用notifyCancelling方法,这个才是最关键的代码

我们看一下这个方法:

private fun notifyCancelling(list: NodeList, cause: Throwable) {
    onCancelling(cause)
    // 1,通知子Job
    notifyHandlers<JobCancellingNode>(list, cause)
    // 2,通知父Job
    cancelParent(cause)
}

这个方法和我们前面所说的伪代码逻辑基本一致,我们分别来看看其中的逻辑:

private inline fun <reified T: JobNode> notifyHandlers(list: NodeList, cause: Throwable?) {
    var exception: Throwable? = null
    list.forEach<T> { node ->
        try {
            node.invoke(cause)
        } catch (ex: Throwable) {
            exception?.apply { addSuppressedThrowable(ex) } ?: run {
                exception =  CompletionHandlerException("Exception in completion handler $node for $this", ex)
            }
        }
    }
    exception?.let { handleOnCompletionException(it) }
}

这里就是遍历当前Job的子Job,并且将取消的case传递过去,这里的invoke()最终会调用ChildHandleNode的invoke()方法:

internal class ChildHandleNode(
    @JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {
    override val parent: Job get() = job
    override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
    override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}

public final override fun parentCancelled(parentJob: ParentJob) {
    cancelImpl(parentJob)
}

而这里代码最终会调用cancelImpl()方法,即前面所说的Job取消的入口函数,这实际上就相当于在递归调用

我们接着看一下如何通知父Job:

private fun cancelParent(cause: Throwable): Boolean {
    if (isScopedCoroutine) return true

    val isCancellation = cause is CancellationException
    val parent = parentHandle

    if (parent === null || parent === NonDisposableHandle) {
        return isCancellation
    }
    // 1
    return parent.childCancelled(cause) || isCancellation
}

注意注释1的返回值,这个返回值是有意义的,返回true代表父协程处理了异常,而返回false,代表父协程没有处理异常。

public open fun childCancelled(cause: Throwable): Boolean {
    if (cause is CancellationException) return true
    return cancelImpl(cause) && handlesException
}

这里我们发现当异常是CancellationException的时候,协程是会进行特殊处理的。一般来说,父协程会忽略子协程的取消异常,当是其他异常时,那么父协程就会响应子协程的取消了。这时又会调用cancelImpl(),来继续递归调用该父协程其他子协程的取消函数。

总结

本篇文章涉及的代码跳转较多,我们做个总结:

  1. 每次创建CoroutineScope的时候,它的内部会确保CoroutineContext当中一定有Job元素,而CoroutineScope就是通过这个Job对象来管理协程的。
  2. 在我们通过launch、async创建协程的时候,会同时创建AbstractCoroutine的子类,在它的initParentJob()方法中,会建立父子关系。每个协程都会对应一个Job,而每个Job都会有一个父Job,多个子Job。最终他们会形成一个N叉树的结构。
  3. 由于协程是一个N叉树的结构,因此协程的取消事件以及异常传播,也会按照这个结构进行传递。每个Job取消的时候,都会通知自己的子Job和父Job,最终以递归的形式传递给每一个协程。
  4. 协程向上取消父Job的时候,还利用了责任链模式,确保取消事件可以一步步传递到顶层的协程。这里还有一个细节就是,默认情况下,父协程会忽略子协程的CancellationException

这里,我们可以进一步总结出协程的结构化取消规律了。

对于CancellationException引起的取消,它只会向下传播,取消子协程;对于其他的异常引起的取消,它既可以向上取消,也可以向下传播,最终会导致所有协程被取消。

协程取消.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值