本系列专栏 #Kotlin协程

前言

前面文章我们介绍了挂起函数,协程借助挂起函数可以以同步的方式写出异步的代码,算是协程最大的亮点。本篇文章将介绍协程的另一个重大特性:结构化并发。

正文

什么是结构化并发,简单来说就是带有结构和层级的并发

父子协程

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

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

fun main() = runBlocking {
    val parentJob: Job
    var job1: Job? = null
    var job2: Job? = null
    var job3: Job? = null
    parentJob = launch {
        job1 = launch {
            delay(1000L)
        }
        job2 = launch {
            delay(3000L)
        }
        job3 = launch {
            delay(5000L)
        }
    }
    delay(500L)
    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() // 这里会挂起大约5秒钟
    logX("Process end!")
}

在代码中我们创建了3个Job分别是job1,job2和job3,然后有个parentJob,我们在parentJob中再次启动3个协程,然后最神奇的地方在下面的打印,我们取出parentJob的children中的job对象,来和前面job1、job2和job3比较会发现是同一个对象。

这也就说明job1、job2和job3是parnetJob的children,也就是说使用launch创建出来的协程,是存在父子关系的

结构化

我们来看一下Job的代码:

public val children: Sequence<Job>
public fun attachChild(child: ChildJob): ChildHandle

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

协程粉碎计划 | 结构化并发

而这个父子协程的关系就可以构成线程所不具备的结构化,我们也注意了前面代码中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()方法时,其子协程也都会取消,这就太方便了。下图有个动图来表示一下:

协程粉碎计划 | 结构化并发

总结

协程的结构化并发意思就是带有结构和层次的并发,而这个层次指的就是协程可以存在父子关系,取消父协程,就会取消其所有子协程。