2.2-结构化并发:父子协程关系的建立

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 对象

  • 父协程会等待所有子协程都执行完之后再结束它自己

  • 自定义协程关系这个功能一定要慎重使用,不然很容易逻辑链条出现问题导致代码执行错乱

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值