Job 获取协程之间的关系
在上节我们分析了 [一个协程] 从不同的视角分析它可以是 Job,也可以是 CoroutineScope。
确定协程父子关系最常见简单的做法是,在 launch 内部调用 launch(确切的说是用外部 launch 参数的 CoroutineScope 调用 launch,this.launch),此时内部的 launch 就是外部 launch 的子协程:
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
var innerJob: Job? = null
val job = scope.launch {
// 也可以写成 this.launch {}
innerJob = launch {
delay(100) // 子协程做个延时,避免协程执行结束自动解绑
}
}
val children = job.children
println("children count: ${children.count()}")
println("innerJob === children.first(): ${innerJob === children.first()}")
println("innerJob.parent === job: ${innerJob?.parent === job}")
}
输出结果:
children count: 1
innerJob === children.first(): true
innerJob.parent === job: true
可以看到打印的 job.children 数量为 1,innerJob 对比 job.children 的引用是同一个对象,innerJob.parent 对比 job 的引用是同一个对象,所以 innerJob 和 job 就是父子协程的关系。
这时候如果我将内部的 launch 启动不用 this,而是另外用 scope 启动会怎样:
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
var innerJob: Job? = null
val job = scope.launch {
// 内部的 launch 用 scope 启动
innerJob = scope.launch {
delay(100)
}
}
val children = job.children
println("children count: ${children.count()}")
println("innerJob === children.first(): ${innerJob === children.first()}")
println("innerJob.parent === job: ${innerJob?.parent === job}")
}
输出结果:
children count: 0
抛出异常
打印 job.children 为 0,且在执行后续的代码抛出了异常,因为 job 和 innerJob 不是父子关系,job 没有 children,尝试获取 children.first() 肯定也就拿不到了,所以抛出了异常。
协程关系的确立
在上面的例子中,我们先创建 CoroutineScope 对象,然后调用 launch 就能启动协程,实际上在创建 CoroutineScope 时也会创建 Job:
CoroutineScope.kt
// context 我们传入的是 EmptyCoroutineContext,context[Job] 是 null
// 会走 else 分支手动创建 Job() 对象
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
var innerJob: Job? = null
// scope.launch 启动的协程 job,是 CoroutineScope 创建的 Job 对象的子协程
val job = scope.launch {
innerJob = scope.launch {
delay(100)
}
}
}
所以 外部的 scope.launch 返回的 Job 对象,是 CoroutineScope 创建的 Job 对象的子协程。
job 和 innerJob 两个协程用的同一个 CoroutineScope 启动协程,它们是兄弟关系而不是父子关系。真正决定父子协程关系的是启动协程时所使用的 Job 对象。
知道了协程之间关系的确立方式,在开发时我们甚至可以自定义 Job 之间的关系:
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
var innerJob: Job? = null
val job = scope.launch {
// 虽然是用 this 这个 CoroutineScope 启动的协程
// 但是用的是自定义的 Job,所以 innerJob 和 job 不是父子关系
// innerJob 是 customJob 的子协程
val customJob = Job()
innerJob = this.launch(customJob) {
delay(100)
}
}
}
上面的例子虽然内部是用的外部 launch 传参的 CoroutineScope 启动的协程,但内部 launch 提供的是一个自定义的 Job 对象,所以外部 Job 和内部 Job 不是父子关系。
协程关系对结构化取消的影响
有了父子协程关系,在结构化并发中的比如结构化取消,父协程会等待所有子协程都执行完之后再结束它自己。
简单说就是子协程被当作是父协程的一部分,哪怕父协程的代码都执行完成了,它也会在所有子协程都结束之后才判断 [我结束了]。
比如用最一开始的例子说明:
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
// 协程之间都是并行执行的
// 但 job 会等待 innerJob 延时执行结束后再结束它自己
val job = scope.launch {
val innerJob = launch {
delay(100) // 子协程做个延时,避免协程执行结束自动解绑
}
}
val startTime = System.currentTimeMillis()
job.join()
val duration = System.currentTimeMillis() - startTime
println("duration: $duration") // 105
}
job 和 innerJob 是父子关系,虽然 job 协程内的代码在启动子协程 innerJob 后就执行结束了,但还是会等待子协程延时 100ms 执行结束后才放行 job.join,打印的时间也符合我们描述的逻辑。
如果我们把它们的关系断开,外部的协程就不会等里面的协程了:
fun main() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {
// innerJob 和 job 不是父子关系
val innerJob = launch(Job()) {
delay(100)
}
}
val startTime = System.currentTimeMillis()
job.join()
val duration = System.currentTimeMillis() - startTime
println("duration: $duration") // 4
}
有了父协程会等待子协程执行结束后再结束自己的这个特点,在实际应用场景中这个特点会很实用。比如另一个协程的任务执行依赖初始化所有子协程工作执行完,就可以用这种关系轻松实现:
// 初始化流程的工作
val initJob = scope.launch {
// 启动子协程执行各个初始化任务
launch {}
launch {}
...
}
scope.launch {
// initJob 会等待所有子协程都结束后自己才结束
// initJob 结束自己后 join 放行能往下执行代码
initJob.join()
...
}
虽然我们能自定义协程之间的关系,但是 自定义协程关系这个功能一定要慎重使用,一般我们不自定义协程关系,除非你很确定自己在做什么,不然很容易逻辑链条出现问题导致代码执行错乱。
总结
-
创建 CoroutineScope 时也会创建 Job 对象,scope.launch 返回的 Job 对象,是 CoroutineScope 创建的 Job 对象的子协程,在 scope.launch 内部调用 this.launch 创建的 Job,是外部 Job 对象的子协程。真正决定父子协程关系的是启动协程时所使用的 Job 对象
-
父协程会等待所有子协程都执行完之后再结束它自己
-
自定义协程关系这个功能一定要慎重使用,不然很容易逻辑链条出现问题导致代码执行错乱