结构化并发不是编译期由代码嵌套结构决定的语法树,而是一棵由 launch(context) 中的 Job 引用在运行时精确构建的动态树
协程的父子关系由 Job 来管理。当你启动一个协程:
val scope = CoroutineScope(Job())
var innerJob: Job? = null
val job = scope.launch {
innerJob = launch {
delay(100)
}
}
// innerJob.parent === job → true
// innerJob === job.children.first() → true
这两个 true 构成了一个双向绑定的树形结构:
innerJob.parent === job 为 true,含义是:子协程明确知道谁是自己的父节点。
当你在 scope.launch { ... } 内部再次调用 launch 时,内部的 launch 会执行以下逻辑:
因此,innerJob.parent 返回的就是创建它的那个外层协程 Job。
innerJob === job.children.first() 为 true,含义是:父协程也明确持有所有子节点的引用。
Job 接口定义了 val children: Sequence<Job> 属性。AbstractCoroutine(Job 的实现基类)内部维护了一个子节点列表。当子协程通过 initParentJob 注册时,它会把自己添加到父 Job 的这个列表中。所以从父节点向下遍历,也能准确找到所有子协程。
父子关系不是由代码的文本嵌套决定的,而是由 launch 时传入的 Context 中的 Job 引用精确构建出来的。看下面三种场景:
val scope = CoroutineScope(Job())
var innerJob: Job? = null
// ———— 场景1:嵌套 launch,默认继承父 Job ————
val job1 = scope.launch {
innerJob = launch {
delay(100) // innerJob.parent === job1 → 父子关系
}
}
// ———— 场景2:显式传入 scope 的 Job ————
val job2 = scope.launch {
innerJob = launch(scope.coroutineContext[Job]!!) {
delay(100) // innerJob.parent === scope的Job → 兄弟关系!
}
}
// ———— 场景3:传入一个孤立的 customJob ————
val job3 = scope.launch {
val customJob = Job()
innerJob = launch(customJob) {
delay(100) // innerJob.parent === customJob → 与job3无关系!
}
}
用树形图直观展示三种场景的差异:
在场景2和场景3中,innerJob 和 job(外层 launch)是兄弟关系,甚至没有关系:
| 场景 | innerJob 的 parent | job 等待 innerJob? | job 取消会取消 innerJob? |
|---|---|---|---|
| 场景1:默认嵌套 launch | job1 | 是 | 是 |
| 场景2:显式传 scope 的 Job | scope 的 Job | 否(scope Job 会等) | 否(job2 不是 innerJob 的父) |
| 场景3:传入孤立 customJob | customJob | 否 | 否(孤立的 Job,无人管理) |
父协程总是会等待所有子协程结束才标记为完成。利用这个特性,我们可以设计一个实验来验证:结构化并发是由 Job 引用在运行时精确构建的,与代码的文本嵌套结构无关。
fun main() = runBlocking {
val test = MyTest()
test.start()
}
class MyTest {
val topScope = CoroutineScope(Dispatchers.Default)
fun start() {
var innerScope: CoroutineScope? = null
val job = topScope.launch {
innerScope = this
// delay(200) 让协程保持活跃,在此期间子协程可以加入协程树
delay(200)
}
// 等待 100ms 确保 job 协程已经启动并进入 delay
Thread.sleep(100)
// 从外部向 job 注入一个子协程!
// innerScope 绑定的 Job 就是 job 自身(this === job)
innerScope!!.launch {
delay(1000) // 这个子协程比父协程的 delay(200) 更晚完成
}
// 阻塞等待 job 完成
runBlocking {
val start = System.currentTimeMillis()
job.join()
val end = System.currentTimeMillis()
println("duration: ${end - start}ms") // 输出约 1002ms
}
}
}
用时间线图展示完整的时间推进过程:
job.join() 等待的时间 ≈ Thread.sleep(100) 之后剩余的时间。子协程的 delay(1000) 从 T≈100 开始,到 T≈1100 结束。job 自身的 delay(200) 在 T≈200 就结束了,但因为有子协程,job 不能完成。所以从 runBlocking 开始计时到 join 返回,大约是 1000ms(加上调度开销约 2ms)。
使用 innerScope.launch 启动的协程,会以 innerScope 绑定的 Job 作为候选 parent。注册结果取决于该 Job 在调用时的状态:
| 候选 parent Job 的状态 | 注册结果 |
|---|---|
| 活跃中(isActive = true) | 注册为真正的子协程,父协程会等待其完成 |
| 完成中(isCompleted = false,但正在等待子协程) | 仍然可以注册为子协程,父协程继续等待 |
| 已完成(isCompleted = true) | 新协程会被立即取消,不会成为子协程,父协程也不会等待 |
| 已取消(isCancelled = true) | 新协程会被立即取消,不会成为子协程 |
在我们的实验中,T=100ms 时 job 仍在 delay(200) 中(活跃状态),所以子协程成功注册。如果在 T=300ms(job 已完成后)再尝试 launch,新协程会被立即取消。
// 验证时机敏感性:如果父协程已完成,注入会失败
fun start() {
var innerScope: CoroutineScope? = null
val job = topScope.launch {
innerScope = this
delay(100) // 很短,很快完成
}
Thread.sleep(500) // 此时 job 肯定已经完成了
// job 已完成,这个 launch 创建的协程会被立即取消
innerScope!!.launch {
delay(1000) // 这段代码不会被执行
println("永远不会打印")
}
runBlocking {
val start = System.currentTimeMillis()
job.join() // 几乎立即返回,因为 job 早已完成
val end = System.currentTimeMillis()
println("duration: ${end - start}ms") // 输出约 0ms
}
}
launch(context) 中的 context[Job] 就是这棵树的唯一构建指令。只要这条指令在正确的时机指向了正确的 Job,无论代码结构如何,父子关系就会成立。
child.parent === parent(子知道父)和 child === parent.children.first()(父持有子),两者共同构成了结构化并发的骨架。结构化并发不是语法糖——它是运行时由 Job 引用精确构建的一棵动态树。代码嵌套只是默认情况下的便利,真正的父子关系永远由 launch(context) 中的 context[Job] 在那一刻指向谁来决定。