协程(6) | 结构化并发

2,655 阅读4分钟

前言

上一篇文章,我们通过Job可以监控协程状态以及控制协程,同时也简单提及了一点就是Job是具有父子关系的,而通过这个特性,Kotlin为协程提供了一个特性:结构化并发,这个特性可以极大地提高我们开发效率。

正文

什么是结构化并发,简单来说就是带有结构和层级的并发,这里的结构和层次指的就是协程的父子关系,而并发操作指的就是前一篇文章我们所说的cancel等操作。

父子协程

在Java中我们知道想并发编程就使用多线程,但是线程和线程之间却是没有父子关系的,而这些协程却是可以有父子关系的。

说起来可能不好理解,我们直接看个例子:

fun main() = runBlocking {
    val parentJob: Job
    var job1: Job? = null
    var job2: Job? = null
    var job3: Job? = null
    //父协程
    parentJob = launch {
        //3个子协程
        job1 = launch {
            delay(1000L)
        }

        job2 = launch {
            delay(3000L)
        }

        job3 = launch {
            delay(5000L)
        }
    }

    delay(500L)
    //遍历父协程的`Job`中的子协程集和
    parentJob.children.forEachIndexed { index, job ->
        when (index) {
            0 -> println("job1 === job is ${job1 === job}")
            1 -> println("job2 === job is ${job2 === job}")
            2 -> println("job3 === job is ${job3 === job}")
        }
    }
    //等待父协程执行完成
    parentJob.join() 
    logX("Process end!")
}

上面代码不难理解,直接看注释,打印结果全是true,这里也就表明parentJob中的children集和中的job和在其协程代码块中创建的job是同一个。

协程通过这种关系,来建立parentJob是其中3个子job的父协程,即协程和协程之间是有父子关系的

结构化

我们来看一下源码,在Job中有如下代码:

public val children: Sequence<Job>

public fun attachChild(child: ChildJob): ChildHandle

可以看到每个Job对象都会有一个children属性,它的类型是Sequence,是一个惰性集合,那么我们就可以用一个简单的图来说明一下上面4个Job之间的关系:

image.png

而这个父子协程的关系就可以构成线程所不具备的结构化并发操作

在前面代码中parentJob.join()会将代码挂起大约5s,而这个5s就是job3执行的时间,所以这就说明只有当其子协程都执行完毕后,parentJob才执行完成

这个就是结构化的完成操作,对于一个协程来说,只有它所有子协程都完成后,它才算是完成状态。

这种关系是线程中不存在的,比如我主线程开启子线程A,子线程A中再开启子线程B,这时子线程A运行完就结束了,不会和A和C有任何关系。

结构化取消操作

还有一个非常重要的结构化操作就是结构化取消。这个在Android开发中至关重要,比如我们使用MVVM架构,我们的逻辑都写在ViewModel中,而ViewModel会在页面销毁后销毁,这时我们使用ViewModel的协程范围即ViewModelScope来启动协程,在协程中做一些操作,不管这个操作调用得多复杂,当调用ViewModelScope.cancel()时把最外面的父协程取消掉,里面所有的子协程业务都会取消,会减少内存泄漏的风险

比如下面代码:

fun main() = runBlocking {
    val parentJob: Job
    var job1: Job? = null
    var job2: Job? = null
    var job3: Job? = null

    parentJob = launch {
        job1 = launch {
            logX("Job1 start!")
            delay(1000L)
            logX("Job1 done!") // ①,不会执行
        }

        job2 = launch {
            logX("Job2 start!")
            delay(3000L)
            logX("Job2 done!") // ②,不会执行
        }

        job3 = launch {
            logX("Job3 start!")
            delay(5000L)
            logX("Job3 done!")// ③,不会执行
        }
    }

    delay(500L)
    
    parentJob.cancel() // 变化在这里
    logX("Process end!")
}

这里还是一个parentJob和它的3个子协程,当我们执行父协程的cancel()方法时,其子协程也都会取消,这就太方便了。下图有个动图来表示一下:

结构化取消.gif

总结

通过协程的句柄Job,已经在创建协程时框架为我们建立的父子关系,让协程具有了结构化并发的操作。而对于取消操作,这个是非常有用的,毕竟能直接取消一大堆嵌套的业务逻辑代码,避免了内存泄漏。