前言
上一篇文章介绍了协程概念的具像化,算是对协程的概念进一步进行加深,本篇文章来看一下协程结构化的概念。
结构化 是协程中一个非常重要且非常实用的一个特性,它可以帮助我们更好的管理协程的生命周期。
如果说 挂起函数 解决了并发任务的写法问题,使得我们可以像写同步代码一样来实现异步逻辑,消除了 回调地狱,降低了 并发任务的复杂度。
那么协程的结构化 则帮我们解决了 并发任务的管理 的问题。
协程的父子关系
之所以说协程是结构化的,是因为协程是存在父子关系的。
一个协程可以有多个子协程,子协程又可以有多个子协程,这样就形成一个树形结构。
在上一篇协程概念具像化的文章中,我们分析了协程创建的过程,并提到了通过 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 中的协程执行完毕。
协程会等待所有的子协程执行完毕后才会结束,这也是协程结构化的一个体现。
后续关于协程的取消,异常处理等内容,都跟协程的父子关系有很强的关联。所以,协程结构化是协程中一个非常重要的知识点,一定要理清楚。
好了,这篇文章就到这里,希望对你有所帮助。
感谢阅读,如果对你有帮助请点赞支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客



















