前言
上一篇文章介绍了协程概念的具像化,算是对协程的概念进一步进行加深,本篇文章来看一下协程结构化的概念。
结构化 是协程中一个非常重要且非常实用的一个特性,它可以帮助我们更好的管理协程的生命周期。
如果说 挂起函数 解决了并发任务的写法问题,使得我们可以像写同步代码一样来实现异步逻辑,消除了 回调地狱,降低了 并发任务的复杂度。
那么协程的结构化 则帮我们解决了 并发任务的管理 的问题。
协程的父子关系
之所以说协程是结构化的,是因为协程是存在父子关系的。
一个协程可以有多个子协程,子协程又可以有多个子协程,这样就形成一个树形结构。
在上一篇协程概念具像化的文章中,我们分析了协程创建的过程,并提到了通过 coroutineScope
中的 launch
函数创建出来的协程的返回值类型是 Job
。这个 Job
中的大部分属性和方法都跟线程中的 Thread
类似,比如 start
,join
,cancel
等。
但是,Job
还有线程没有对等概念的属性例如: parent
,children
,而 parent
和 children
就是体现协程结构化的关键,从名字上也可以看出来,parent
是父协程,children
是子协程。
先来看一段代码
fun main() = runBlocking {
//创建一个协程作用域
val scope = CoroutineScope(Dispatchers.IO)
var childJob: Job? = null
//启动一个协程
val job = scope.launch {
println("父协程")
//启动一个子协程
childJob = launch {
delay(100)
println("子协程")
}
}
val childrenJob = job.children
println("childrenJob.count = ${childrenJob.count()}")
println("childrenJob.first() === childJob = ${childrenJob.first() === childJob}")
println("childJob?.parent===job = ${childJob?.parent === job}")
//等待协程结束
job.join()
}
输出结果:
可以看到,我们通过 CoroutineScope
创建了一个协程作用域,然后通过 launch
函数启动了一个协程,再直接在 launch
代码块中又通过 launch
函数启动了一个子协程。
通过打印的结果可以看到 job
和 childJob
是父子关系。
父子关系的建立
那么,这个父子关系是怎么建立的呢?
我们来跟下源码:
还是以 launch
函数为例,这个 launch
函数在 协程概念具像化 这篇文章中详细分析过了,这里就不说得太细了。
首先,launch
函数会创建一个 StandaloneCoroutine
对象,这个对象最终以 Job
的形式返回。
再来看看 StandaloneCoroutine
StandaloneCoroutine
继承自 AbstractCoroutine
,注意这里传过去的参数是 parentContext
,这个参数就是父协程的 context
,initParentJob
的值为 true。
再来看看 AbstractCoroutine
的 initParentJob
函数:
initParentJob
实际上就是用来建立父子关系的,它会将父协程的 Job
传递过去,在内部做处理,建立父子关系。
initParentJob
是 JobSupport
类中的一个方法,该方法中做了两件事:
parent.attachChild(this)
:将当前协程添加到父协程的children
属性中this.parent = parent
:将父协程赋值给当前协程的parent
属性
这样一来,父协程就可以通过 children
属性来获取自己的所有子协程了。而子协程也可以通过 parent
属性来获取自己的父协程。
也就是说,父子协程的建立是通过 Job
的 parent
和 children
属性来实现的。决定协程父子关系的关键点是 Job
对象。
理清楚了父子关系的建立逻辑,我们回过头来想一下,最开始的父协程的 Job
对象是哪来的呢?我在代码中并没有显式的创建父协程的 Job
对象啊。
再来回过头来看一下之前的代码:
我最开始是通过 CoroutineScope
创建了一个协程作用域,然后通过 launch
函数启动了一个协程,这个协程就是父协程。
那么,这个父协程的 Job
对象是怎么来的呢?
看下 CoroutineScope
的源码:
可以看到,如果我们没有显式的传递 Job
对象,那么 CoroutineScope
会自动创建一个 Job
对象,这个 Job
对象就是父协程的 Job
对象。
在 launch
中创建协程对象之前,会先执行 CoroutineScope.newCoroutineContext
方法。
这个方法会把 scope 中的 coroutineContext
也就是我们创建出来的 CoroutineScope
中的coroutineContext
跟 EmptyCoroutineContext
合并,生成一个新的 CoroutineContext
,然后传递给 StandaloneCoroutine
的构造函数。
那这样一来,StandaloneCoroutine
就会拿到这个 CoroutineContext
,然后通过 CoroutineContext
获取到 Job
对象,这个 Job
对象就是父协程的 Job
对象。
至此,Job
是怎么来的,以及父子关系是怎么建立的,我们都已经分析清楚了。
下面看一下多个父子关系的情况来巩固一下:
fun main() = runBlocking {
//创建一个协程作用域
val scope = CoroutineScope(Dispatchers.IO)
var childJob: Job? = null
var childJob2: Job? = null
var childJob3: Job? = null
//
//启动一个协程,job 是由 CoroutineScope 创建的,所以job的父协程是scope
val job = scope.launch {
println("父协程")
//启动一个子协程,childJob的父协程是job,也就是说job跟childJob是父子关系
childJob = launch {
//启动一个子协程,child3的父协程是childJob,也就是说childJob3跟childJob是父子关系
childJob3 = launch {
delay(100)
println("子协程的子协程")
}
delay(100)
println("子协程")
}
//启动一个子协程,childJob2的父协程是job,也就是说job跟childJob2是父子关系.childJob和childJob2是兄弟关系
childJob2 = launch {
delay(100)
println("子协程2")
}
}
// delay(200)
val childrenJob = job.children
val scopeChildrenJobs = scope.coroutineContext[Job]?.children
println("scopeChildrenJobs.count = ${scopeChildrenJobs?.count()}")
println("scopeChildrenJobs?.first()===job = ${scopeChildrenJobs?.first() === job}")
println("childrenJob.count = ${childrenJob.count()}")
println("childJob?.parent===job = ${childJob?.parent === job}")
println("childJob2?.parent===job = ${childJob2?.parent === job}")
println("childJob3?.parent===childJob = ${childJob3?.parent === childJob}")
//等待协程结束
job.join()
}
在上面的代码中:
- 通过
scope.launch
创建出来的job
,他的父Job
对象是scope
中的Job
对象,因此,job
是scope
的子协程。 job
中通过launch
创建出来的childJob
,他的父Job
对象是job
,因此,childJob
是job
的子协程。job
中通过launch
创建出来的childJob2
,他的父Job
对象是job
,因此,childJob2
是job
的子协程。childJob
中通过launch
创建出来的childJob3
,他的父Job
对象是childJob
,因此,childJob3
是childJob
的子协程。childJob
和childJob2
是兄弟关系,他们的父Job
对象都是job
。
执行结果:
这样一来,他们就形成一个树形结构如下:
总结
- 协程是存在父子关系的,父协程可以有多个子协程,子协程也可以有多个子协程,形成一个树形结构。
- 父子关系是通过
Job
对象的parent
和children
来维护的,Job
对象是确定父子关系的关键。
在我们日常写代码的时候,建议遵循以下原则:
- 正常在一个协程中直接启动子协程,这样默认就是父子关系,方便管理。
- 尽量不要在一个协程中启动不是它的子协程的协程,这样会导致协程的父子关系混乱,不利于协程的管理。
类似下面的代码就会破坏父子关系:
fun main() = runBlocking {
//创建一个协程作用域
val scope = CoroutineScope(Dispatchers.IO)
var job1: Job? = null
var job2: Job? = null
val job = scope.launch {
println("父协程")
println("job:${coroutineContext.job}")
//这里的协程虽然是在嵌套的launch中创建的,但是是通过scope创建的,因此,job1的父协程是也是scope,而不是job
job1 = scope.launch {
println("job1:${coroutineContext.job.parent}")
}
//这里虽然是在嵌套的launch中创建的,但是传递了一个新的Job,因此job2的父协程也不是job
job2 = launch(Job()) {
println("job2:${coroutineContext.job.parent}")
}
}
val childrenJob = job.children
println("childrenJob.count = ${childrenJob.count()}")
//等待协程结束
job.join()
}
输出结果:
可以看到,虽然代码结构是嵌套的,但是 job1
和 job2
的父协程并不是 job
,也就不存在父子关系了,这样的代码结构是不利于协程的管理的。
最后提一下上面的代码中为什么最后要调用 job.join()
runBlocking
函数是一个挂起函数,它会阻塞当前线程,直到他内部的所有子协程都执行完毕,但是我们写的代码是通过自己创建的 CoroutineScope
来启动协程的,也就是说,我们后续创建的协程都是 CoroutineScope
的子协程,而不是 runBlocking
的子协程,那么 runBlocking
并不会等待 CoroutineScope
中的协程执行完毕,所以我们需要手动调用 job.join()
来等待 CoroutineScope
中的协程执行完毕。
协程会等待所有的子协程执行完毕后才会结束,这也是协程结构化的一个体现。
后续关于协程的取消,异常处理等内容,都跟协程的父子关系有很强的关联。所以,协程结构化是协程中一个非常重要的知识点,一定要理清楚。
好了,这篇文章就到这里,希望对你有所帮助。
感谢阅读,如果对你有帮助请点赞支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客