在前几篇文章都是在介绍协程的作用域,也经常提及协程的父子关系,那么什么样的协程关系才是父子协程关系呢
先看如下代码
@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的传播机制,我们就可以去了解协程的异常了,由于协程异常的特殊性,所有需要非常多的铺垫