前言
这篇文章来介绍一下协程结构化并发的原理。
正文
在开始之前,我们可以回忆一下,协程的结构化并发就是带有层级和结构的并发,而这个层次和结构,在前面有篇文章:
这里有介绍,简单来说就是协程存在父子关系,而这里父子关系我们使用协程句柄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。
结构化取消
既然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(),来继续递归调用该父协程其他子协程的取消函数。
总结
本篇文章涉及的代码跳转较多,我们做个总结:
- 每次创建CoroutineScope的时候,它的内部会确保CoroutineContext当中一定有Job元素,而CoroutineScope就是通过这个Job对象来管理协程的。
- 在我们通过launch、async创建协程的时候,会同时创建AbstractCoroutine的子类,在它的initParentJob()方法中,会建立父子关系。每个协程都会对应一个Job,而每个Job都会有一个父Job,多个子Job。最终他们会形成一个N叉树的结构。
- 由于协程是一个N叉树的结构,因此协程的取消事件以及异常传播,也会按照这个结构进行传递。每个Job取消的时候,都会通知自己的子Job和父Job,最终以递归的形式传递给每一个协程。
- 协程向上取消父Job的时候,还利用了责任链模式,确保取消事件可以一步步传递到顶层的协程。这里还有一个细节就是,默认情况下,父协程会忽略子协程的CancellationException。
这里,我们可以进一步总结出协程的结构化取消规律了。
对于CancellationException引起的取消,它只会向下传播,取消子协程;对于其他的异常引起的取消,它既可以向上取消,也可以向下传播,最终会导致所有协程被取消。