在前几篇文章都是在介绍协程的作用域,也经常提及协程的父子关系,那么什么样的协程关系才是父子协程关系呢
先看如下代码
kotlin
@JvmStatic
fun main(array: Array<String>){
//顶级父协程
runBlocking {
//子协程1
launch {
delay(1000)
println("launch 1 子协程执行完毕")
}
//子协程2
launch {
delay(2000)
println("launch 2 子协程执行完毕")
}
}
}
结果:
launch 1 子协程执行完毕
launch 2 子协程执行完毕
Process finished with exit code 0
可以发现,在 runBlocking 协程作用域内,我们创建了2个新的协程,这两个协程都进行了 delay,虽然 runBlocking 没有等待 launch 1 与 launch 2 的执行完毕,但是结果都被打印出来了,这就证明 launch 1 与 launch 2 协程被 顶级父协程所管理,这种现象就是 父子协程关系,那么如何建立父子协程关系呢,
想要创建父子协程关系,子协程必须创建在父协程的作用域内,并且创建时不能重新指定 CoroutineContext ,否则 新创建的协程将构建自己的协程作用域,从而升级与外部协程并立成兄弟协程
为了验证上面这段话,现在将例子重新修改一下,为了让大家看得更直观,例子里面包含了一个完整版本,一个缩写版本
scss
@JvmStatic
fun main(array: Array<String>){
//顶级父协程
runBlocking {
//缩写版(由于第一个参数就是 context ,我们可以省略 context = )-->由于重新指定了 CoroutineContext ,升级为 runBlocking 的兄弟协程
launch(Job()) {
delay(1000)
println("launch 1 协程执行完毕")
}
//由于重新指定了 CoroutineContext ,升级为 runBlocking 的兄弟协程
launch(context = Job()) {
delay(2000)
println("launch 2 协程执行完毕")
}
}
}
结果:
Process finished with exit code 0
发现在重新指定了context 是Job()的情况下, launch 1 与 launch 2 都没有重新打印,那么是不是只要替换了 CoroutineContext 就肯定会出现何种情况呢
看看下面这种情况
scss
@JvmStatic
fun main(array: Array<String>){
//顶级父协程
runBlocking(Dispatchers.Default){
//子协程1
launch(Dispatchers.IO) {
delay(1000)
println("launch 1 子协程执行完毕")
}
//子协程2
launch(context = Dispatchers.IO) {
delay(2000)
println("launch 2 子协程执行完毕")
}
}
}
结果:
launch 1 子协程执行完毕
launch 2 子协程执行完毕
Process finished with exit code 0
指定 runBlocking 的context 是 Dispatchers.Default,重新指定子协程的context 是 Dispatchers.IO,所有协程的结果都能打印出来,这里还不能确定,我们现在让 runBlocking 在0.5s后取消,看看 launch 1 与 launch 2 的表现
scss
@JvmStatic
fun main(array: Array<String>){
//顶级父协程
runBlocking(Dispatchers.Default){
//子协程1
launch(Dispatchers.IO) {
delay(1000)
println("launch 1 子协程执行完毕")
}
//子协程2
launch(context = Dispatchers.IO) {
delay(2000)
println("launch 2 子协程执行完毕")
}
delay(500)
cancel()
}
}
结果:
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@e2144e4
at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:1555)
at kotlinx.coroutines.CoroutineScopeKt.cancel(CoroutineScope.kt:287)
at kotlinx.coroutines.CoroutineScopeKt.cancel$default(CoroutineScope.kt:285)
at com.tsm.opencv.Tsm$main$1.invokeSuspend(Tsm.kt:43)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Process finished with exit code 1
可以看到父协程被取消 launch 1 与 launch 2 同时也被取消了,即可证明他们现在也是父子协程关系
再来看下面的例子
scss
@JvmStatic
fun main(array: Array<String>){
//顶级父协程
runBlocking{
println(Thread.currentThread().name)
//子协程1
launch {
withContext(Dispatchers.IO){
delay(1000)
println(Thread.currentThread().name)
}
println("launch 1 子协程执行完毕")
}
//子协程2
launch {
withContext(Dispatchers.IO){
delay(1000)
println(Thread.currentThread().name)
}
println("launch 2 子协程执行完毕")
}
}
}
结果:
main @coroutine#1
DefaultDispatcher-worker-1 @coroutine#3
DefaultDispatcher-worker-3 @coroutine#2
launch 2 子协程执行完毕
launch 1 子协程执行完毕
Process finished with exit code 0
为什么已经在子协程中使用了 Dispatchers.IO ,但是 runBlocking 还是会等待 launch 1 与 launch 2 这两个协程呢, 我们先来一步一步分析 1: runBlocking 是在主线程中创建的,所以我们第一行打印的线程名称是 main
2: 我们使用 runBlocking 的作用域 CoroutineScope 创建了子协程 launch 1 与 launch 2
3:我们调用 withContext 方法,并指定了 Dispatchers.IO ,注意 withContext 是一个方法,并不是重新创建一个协程,
所以建立父子协程关系的条件就是 子协程使用外部协程的作用域 CoroutineScope ,并且不重写 CoroutineContext , 但是从上面的例子中发现有一个特殊的,那就是 父协程 CoroutineContext 只是 Dispatchers.Default ,而子协程只使用 Dispatchers.IO 后,他们仍然是父子协程关系
为了证明如果父协程中除了指定 Dispatchers.Default外,增加其他参数条件就不成立,我又修改了一下这个案例
kotlin
@JvmStatic
fun main(array: Array<String>){
runBlocking {
var error=CoroutineExceptionHandler { _, e ->
println("e:$e")
}
var name=CoroutineName("tsm")
var context= name + Dispatchers.IO + error
//由于使用了新的 CoroutineContext 升级成为runBlocking的兄弟协程, 内部launch父协程
CoroutineScope(context).launch {
launch(name+Dispatchers.Default+error) {
//子协程
delay(3000)
}
}
delay(2000)
}
}
结果:
Process finished with exit code 0
了解了协程作用域,协程父子关系,协程CoroutineContext的传播机制,我们就可以去了解协程的异常了,由于协程异常的特殊性,所有需要非常多的铺垫