前面看异常分发时,经常会提到“子协程把异常交给父协程”“父协程取消其它子协程”。这些说法背后都依赖同一件事:父 Job 和子 Job 在协程启动时就已经建立了联系。
这篇只看这条关系是怎么建立起来的。异常怎么分发、CoroutineExceptionHandler 什么时候回调,后面再接着看。
先看一个例子
|
|
这里 parentJob 是父 Job,job1、job2 是两个子 Job。它们不是靠变量名建立关系,而是因为 scope 的 CoroutineContext 里带着 parentJob。后面每次调用 scope.launch 时,新协程都会从 context 里取出这个父 Job,再把自己挂上去。
launch 创建子 Job
launch 内部会先合并当前 CoroutineScope 的 context 和传进来的 context,然后创建 StandaloneCoroutine:
|
|
StandaloneCoroutine 本身就是 Job。所以 launch 返回的 job1、job2,实际都是 StandaloneCoroutine 对象。
关键点在构造函数。StandaloneCoroutine 继承自 AbstractCoroutine,而 AbstractCoroutine 初始化时会调用 initParentJob:
|
|
注意这里取的是 parentContext[Job],不是当前协程自己的 context[Job]。因为当前协程创建出来后,它自己也会作为一个 Job 放进 context,如果从自己的 context 里取,拿到的就会是自己。父子关系必须从创建它之前的 parent context 里取父 Job。
initParentJob 做了什么
initParentJob 的逻辑可以简化成这样:
|
|
如果没有父 Job,比如 GlobalScope.launch,parentHandle 就会指向 NonDisposableHandle,说明这个协程没有普通父 Job。
如果能从 parentContext 里拿到父 Job,就会调用 parent.attachChild(this)。这里的 this 是当前正在初始化的子协程,也就是 job1 或 job2。
attachChild 如何绑定父子关系
attachChild 会创建一个 ChildHandleNode。这个节点很关键,它同时知道父 Job 和子 Job:
|
|
JobNode 里有一个 job 字段,它会指向父 Job。ChildHandleNode 自己又保存了 childJob,指向当前子 Job。attachChild 返回的就是这个节点,子 Job 会把它保存到自己的 parentHandle 里。
所以关系建立完后是这样的:
|
|
父 Job 的 state 里挂着子节点,子 Job 的 parentHandle 又反向持有这个节点。这样父找得到子,子也找得到父。
多个子 Job 时 state 会变成 NodeList
如果只有一个子 Job,父 Job 的 state 里可以直接挂一个 ChildHandleNode。如果继续启动 job2、job3,父 Job 的 state 会升级成 NodeList,所有子节点都挂在这个链表里:
|
|
这也是为什么父 Job 能统一管理多个子协程。它不是直接持有一个 children 集合,而是把子协程对应的 ChildHandleNode 挂在自己的 state 链表里。
子协程如何找到父协程
子协程发生异常时,会进入自己的完成流程。异常处理里有一步是 cancelParent(cause),它会通过 parentHandle 找到父 Job:
|
|
所以“子协程把异常交给父协程”不是抽象说法,它实际就是通过 parentHandle -> ChildHandleNode -> parentJob.childCancelled 这条链路完成的。
父 Job 收到异常后,会根据自己的类型决定怎么处理。普通 JobImpl 会进入取消流程,SupervisorJobImpl 的 childCancelled 直接返回 false,所以不会因为某个子协程失败就取消其它子协程。
父协程如何取消子协程
父 Job 被取消时,会遍历自己 state 里的节点。如果节点是 ChildHandleNode,就会调用它的 invoke,最后执行到子 Job 的 parentCancelled:
|
|
所以“父协程取消子协程”走的是反方向:父 Job 从自己的 NodeList 找到每个 ChildHandleNode,再通过 childJob 找到对应子协程。
ChildCompletion 不是父子关系本身
后面看异常传播时还会遇到 ChildCompletion。它容易和 ChildHandleNode 混在一起。
ChildHandleNode 是父子关系建立时创建的节点,用来让父子 Job 互相找到。ChildCompletion 是父 Job 完成时发现还有子 Job 没结束,临时挂到子 Job 上的完成回调,用来等子 Job 收尾后继续父 Job 的完成流程。
简单区分:
|
|
总结
CoroutineScope(Job() + context) 启动子协程时,父 Job 会先放在 scope 的 CoroutineContext 里。launch 创建 StandaloneCoroutine 后,AbstractCoroutine 构造阶段会从 parentContext[Job] 取出父 Job,然后调用 initParentJob。
如果父 Job 不存在,子协程的 parentHandle 会是 NonDisposableHandle。如果父 Job 存在,initParentJob 会调用 parent.attachChild(child),创建 ChildHandleNode。这个节点的 job 指向父 Job,childJob 指向子 Job,子 Job 的 parentHandle 再反向持有这个节点。
多个子协程启动后,父 Job 的 state 会升级成 NodeList,里面挂着多个 ChildHandleNode。子协程异常时,通过 parentHandle.childCancelled 找到父 Job;父协程取消时,通过遍历 NodeList 找到所有子 Job。协程的父子关系就是靠这条链路建立起来的。