协程学习(七)协程简单的使用之父子协程的层级关系

585 阅读4分钟

在前几篇文章都是在介绍协程的作用域,也经常提及协程的父子关系,那么什么样的协程关系才是父子协程关系呢

先看如下代码

@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 协程被 顶级父协程所管理,这种现象就是 父子协程关系,那么如何建立父子协程关系呢,

想要创建父子协程关系,子协程必须创建在父协程的作用域内,并且创建时不能重新指定 Job ,否则 新创建的协程将构建自己的协程作用域,从而升级与外部协程并立成兄弟协程

为了验证上面这段话,现在将例子重新修改一下,为了让大家看得更直观,例子里面包含了一个完整版本,一个缩写版本

@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 的情况

看看下面替换 Dispatchers 这种情况

@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 的表现

@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 同时也被取消了,即可证明他们现在也是父子协程关系

再来看下面的例子

@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 ,并且不重写 Job

在后续试验过指定 CoroutineErrorHandler 子协程指定了,也不会让子协程升级成功父协程,

  @JvmStatic
    fun main(array: Array<String>) {
        runBlocking {

            var error= CoroutineExceptionHandler { _, e ->
                println("接受到异常 e :$e  其他异常 e:${e.suppressed.contentToString()}")
            }
            
            var job1 = launch {
                launch {
                    launch {
                        launch {
                            delay(100)
                        }
                    }
                }
            }

            var job2 = launch(error) {
                launch {
                    launch {
                        launch {
                            launch {
                                delay(1000)
                                println("--------job2 执行完成-----------")
//                                    throw ClassCastException("报错了")
                            }
                        }
                    }
                }
            }
            delay(100)
            cancel()
            println("job1  isActive ${job1.isActive}     isCancelled ${job1.isCancelled}")
            println("job2  isActive ${job2.isActive}     isCancelled ${job2.isCancelled}")
            }
    }
    
   
    结果:
    job1  isActive false     isCancelled true
job2  isActive false     isCancelled true
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@591f989e
	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:48)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:233)
	at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
	at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:476)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:510)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:499)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:597)
	at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:493)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:280)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.tsm.opencv.Tsm.main(Tsm.kt:11)

Process finished with exit code 1

可以看到父协程取消后,子协程 launch 1 与 launch 2 也同时被取消了,证明了他们之间的父子关系

了解了协程作用域,协程父子关系,协程CoroutineContext的传播机制,我们就可以去了解协程的异常了,由于协程异常的特殊性,所有需要非常多的铺垫