Kotlin协程与Job

126 阅读6分钟

Job与协程关系:

Job 其实就是协程的句柄。从某种程度上讲,当我们用 launch 和 async 创建一个协程以后,同时也会创建一个对应的 Job 对象。

Job 生命周期

launch、async 返回值类型分别是 Job 和 Deferred。

// 代码段1

public interface Deferred<out T> : Job {
    public suspend fun await(): T
}

以上Deferred 的源代码,它其实也是继承自 Job 的。对应的,它只是多了一个泛型参数 T,还多了一个返回类型为 T 的 await() 方法。所以,不管是 launch 还是 async,它们本质上都会返回一个 Job 对象。

通过 Job 对象,要可以做两件事情:

使用 Job 监测协程的生命周期状态;

使用 Job 操控协程。

举例:

// 代码段2

fun main() = runBlocking {
    val job = launch {
        delay(1000L)
    }
    job.log()       // ①
    job.cancel()    // ②
    job.log()       // ③
    delay(1500L)
}

/**
 * 打印Job的状态信息
 */
fun Job.log() {
    logX("""
        isActive = $isActive
        isCancelled = $isCancelled
        isCompleted = $isCompleted
    """.trimIndent())
}

/**
 * 控制台输出带协程信息的log
 */
fun logX(any: Any?) {
    println("""
================================
$any
Thread:${Thread.currentThread().name}
================================""".trimIndent())
}


/*
输出结果:
================================
isActive = true
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
isActive = false
isCancelled = true
isCompleted = false
Thread:main @coroutine#1
================================
*/

定义一个 Job.log() 扩展函数,它的作用就是打印 Job 的生命周期状态。通过调用这个函数,我们就可以知道对应的协程处于什么状态。

注释①处的调用结果,“isActive = true”,这代表了当前的协程处于活跃状态。注释②,我们调用了 job.cancel() 以后,协程任务就会被取消。因此,注释③处的调用结果就会变成“isCancelled = true”,这代表了协程任务处于取消状态。

job.log(),其实就是在监测协程;job.cancel(),其实就是在操控协程。

除了 job.cancel() 可以操控协程以外,我们还经常使用 job.start() 来启动协程任务,一般搭配“CoroutineStart.LAZY”来使用。


fun main() = runBlocking {
    //                  变化在这里
    //                      ↓
    val job = launch(start = CoroutineStart.LAZY) {
        logX("Coroutine start!")
        delay(1000L)
    }
    delay(500L)     
    job.log()       
    job.start()     // 变化在这里
    job.log()
    delay(500L)
    job.cancel()
    delay(500L)
    job.log()
    delay(2000L)
    logX("Process end!")
}

/*
输出结果:
================================
isActive = false
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
isActive = true
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
Coroutine start!
Thread:main @coroutine#2
================================
================================
isActive = false
isCancelled = true
isCompleted = true
Thread:main @coroutine#1
================================
================================
Process end!
Thread:main @coroutine#1
================================
*/

当使用 CoroutineStart.LAZY 作为启动模式的时候,协程任务被 launch 以后,并不会立即执行,即使我们在代码中 delay 了 500 毫秒,launch 内部的"Coroutine start!"也仍然没有输出。这是典型的懒加载行为模式。

在外部调用了 job.start() 以后,job 的状态才变成了 Active 活跃。而当调用了 cancel 以后,job 的状态才变成 isCancelled、isCompleted。

当调用 cancel 以后,isCancelled = true、isCompleted = true。因为Job 内部私有的 Completed、Cancelled 状态,都会认为是外部的 isCompleted 状态。

为了更加灵活地等待和监听协程的结束事件,我们可以用 job.join() 以及 invokeOnCompletion {}

// 代码段6

fun main() = runBlocking {
    suspend fun download() {
        // 模拟下载任务
        val time = (Random.nextDouble() * 1000).toLong()
        logX("Delay time: = $time")
        delay(time)
    }
    val job = launch(start = CoroutineStart.LAZY) {
        logX("Coroutine start!")
        download()
        logX("Coroutine end!")
    }
    delay(500L)
    job.log()
    job.start()
    job.log()
    job.invokeOnCompletion {
        job.log() // 协程结束以后就会调用这里的代码
    }
    job.join()      // 等待协程执行完毕
    logX("Process end!")
}

/*
运行结果:
================================
isActive = false
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
isActive = true
isCancelled = false
isCompleted = false
Thread:main @coroutine#1
================================
================================
Coroutine start!
Thread:main @coroutine#2
================================
================================
Delay time: = 252
Thread:main @coroutine#2
================================
================================
Coroutine end!
Thread:main @coroutine#2
================================
================================
isActive = false
isCancelled = false
isCompleted = true
Thread:main @coroutine#2
================================
================================
Process end!
Thread:main @coroutine#1
================================
*/

invokeOnCompletion {} 的作用,其实就是监听协程结束的事件。需要注意的是,它和前面的 isCompleted 类似,如果 job 被取消了,invokeOnCompletion {} 这个回调仍然会被调用。

job.join() 其实是一个“挂起函数”,它的作用就是:挂起当前的程序执行流程,等待 job 当中的协程任务执行完毕,然后再恢复当前的程序执行流程。

Job 的源代码:

// 代码段7

public interface Job : CoroutineContext.Element {

    // 省略部分代码

    // ------------ 状态查询API ------------

    public val isActive: Boolean

    public val isCompleted: Boolean

    public val isCancelled: Boolean

    public fun getCancellationException(): CancellationException

    // ------------ 操控状态API ------------

    public fun start(): Boolean

    public fun cancel(cause: CancellationException? = null)

    public fun cancel(): Unit = cancel(null)

    public fun cancel(cause: Throwable? = null): Boolean

    // ------------ 等待状态API ------------

    public suspend fun join()

    public val onJoin: SelectClause0

    // ------------ 完成状态回调API ------------

    public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

    public fun invokeOnCompletion(
        onCancelling: Boolean = false,
        invokeImmediately: Boolean = true,
        handler: CompletionHandler): DisposableHandle

}

Kotlin 官方对 Job 的 API 做了更加详细的划分,但实际上来说都属于“监测状态”“操控状态”这两个大的范畴。

如何理解“Job 是协程的句柄”这句话呢?可以从现实生活中找例子:Job 和协程的关系,就有点像“遥控器和空调的关系”。空调遥控器可以监测空调的运行状态;Job 也可以监测协程的运行状态;空调遥控器可以操控空调的运行状态,Job 也可以简单操控协程的运行状态。所以,从某种程度来讲,遥控器也是空调对外暴露的一个“句柄”。

Deferred

Deferred 其实就是继承自 Job 的一个接口,它并没有在 Job 的基础上扩展出很多其他功能,最重要的就是 await() 这个方法。举例:

// 代码段8

fun main() = runBlocking {
    val deferred = async {
        logX("Coroutine start!")
        delay(1000L)
        logX("Coroutine end!")
        "Coroutine result!"
    }
    val result = deferred.await()
    println("Result = $result")
    logX("Process end!")
}

/*
输出结果:
================================
Coroutine start!
Thread:main @coroutine#2
================================
================================
Coroutine end!
Thread:main @coroutine#2
================================
Result = Coroutine result!
================================
Process end!
Thread:main @coroutine#1
================================
*/

deferred.await() 这个方法,不仅可以获取协程的执行结果,它还会阻塞当前协程的执行流程,直到协程任务执行完毕。在这一点的行为上,await() 和 join() 是类似的。

await() 的函数签名:

// 代码段9

public interface Deferred<out T> : Job {
//          注意这里
//            ↓
    public suspend fun await(): T
}

await() 这个方法其实是一个挂起函数,这也就意味着,这个方法拥有挂起和恢复的能力。如果当前的 Deferred 任务还没执行完毕,那么,await() 就会挂起当前的协程执行流程,等待 Deferred 任务执行完毕,再恢复执行后面剩下的代码。

一个线程上可以并行执行多个task,await阻塞的是它所在的task的后续程序的执行,而不阻塞在这个线程上的其它task的执行。await() 后面的代码,虽然看起来是阻塞了,但它只是执行流程被挂起和恢复的一种表现。同样。job.join() 的行为模式,在协程执行完毕之前,后面的协程代码都被暂时挂起了,等到协程执行完毕,才有机会继续执行。

Deferred 只是比 Job 多了一个 await() 挂起函数而已,通过这个挂起函数,我们可以等待协程执行完毕的同时,还可以直接拿到协程的执行结果。

Job 与结构化并发

Kotlin 协程的结构化并发,重要性仅次于“挂起函数”。“结构化并发”是 Kotlin 协程的第二大优势。

“结构化并发”就是:带有结构和层级的并发。

举例:

// 代码段10

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!")
}

/*
输出结果:
job1 === job is true
job2 === job is true
job3 === job is true
// 等待大约5秒钟
================================
Process end!
Thread:main @coroutine#1
================================
*/

上面定义了 4 个 Job,parentJob 是最外层的 launch 返回的对象,而在这个 launch 的内部,还额外嵌套了三个 launch,它们的 Job 对象分别赋值给了 job1、job2、job3。接着,我们对“parentJob.children”进行了遍历,然后逐一对比了它们与 job1、job2、job3 的引用是否相等(“===”代表了引用相等,即是否是同一个对象)。通过这样的方式,我们可以确定,job1、job2、job3 其实就是 parentJob 的 children。也就是说,我们使用 launch 创建出来的协程,是存在父子关系的。

Job 的源代码,有两个 API 是用来描述父子关系的:

// 代码段11

public interface Job : CoroutineContext.Element {
    // 省略部分代码

    // ------------ parent-child ------------

    public val children: Sequence<Job>

    @InternalCoroutinesApi
    public fun attachChild(child: ChildJob): ChildHandle
}

每个 Job 对象,都会有一个 children 属性,它的类型是 Sequence,它是一个惰性的集合,我们可以对它进行遍历。而 attachChild() 则是一个协程内部的 API,用于绑定 ChildJob 的。

调用 parentJob 的 join() 方法,它会等待其内部的 job1、job2、job3 全部执行完毕,才会恢复执行。换句话说,只有当 job1、job2、job3 全部执行完毕,parentJob 才算是执行完毕了。

将“parentJob.join”改为“parentJob.cancel()”。即使调用的只是 parentJob 的 cancel() 方法,并没有碰过 job1、job2、job3,但是它们内部的协程任务也全都被取消了。

以结构化的方式构建协程以后,我们的 join()、cancel() 等操作,也会以结构化的模式来执行。

实战举例:

// 代码段13

fun main() = runBlocking {
    suspend fun getResult1(): String {
        delay(1000L) // 模拟耗时操作
        return "Result1"
    }

    suspend fun getResult2(): String {
        delay(1000L) // 模拟耗时操作
        return "Result2"
    }

    suspend fun getResult3(): String {
        delay(1000L) // 模拟耗时操作
        return "Result3"
    }

    val results = mutableListOf<String>()

    val time = measureTimeMillis {
        results.add(getResult1())
        results.add(getResult2())
        results.add(getResult3())
    }
    println("Time: $time")
    println(results)
}

/*
输出结果:
Time: 3018
[Result1, Result2, Result3]
*/

使用 async 来优化:

// 代码段14

fun main() = runBlocking {
    suspend fun getResult1(): String {
        delay(1000L) // 模拟耗时操作
        return "Result1"
    }

    suspend fun getResult2(): String {
        delay(1000L) // 模拟耗时操作
        return "Result2"
    }

    suspend fun getResult3(): String {
        delay(1000L) // 模拟耗时操作
        return "Result3"
    }

    val results: List<String>

    val time = measureTimeMillis {
        val result1 = async { getResult1() }
        val result2 = async { getResult2() }
        val result3 = async { getResult3() }

        results = listOf(result1.await(), result2.await(), result3.await())
    }

    println("Time: $time")
    println(results)
}

/*
输出结果:
Time: 1032
[Result1, Result2, Result3]
*/

async 最常见的使用场景是:与挂起函数结合,优化并发。

如果任务是 IO 密集型的,代码运行效率是可以实现成倍提升的。万一任务在某些场景下,并发反而会降低效率,也完全可以使用 CoroutineStart 来控制它的启动模式。所以,这种方式的扩展性和灵活性都很好。

小结:

协程是有生命周期的,协程是结构化的。

Job,相当于协程的句柄,Job 与协程的关系,有点像“遥控器与空调的关系”。

Job,在它的内部,维护了一系列的生命周期状态,它也对应着协程的生命周期状态。

通过 Job,我们可以监测协程的状态,比如 isActive、isCancelled、isCompleted;另外,我们也可以一定程度地操控协程的状态,比如 start()、cancel()。

除此之外,我们还可以通过 Job.invokeOnCompletion {} 来监听协程执行完毕的事件;通过 Job.join() 这个挂起函数,我们可以挂起当前协程的执行流程,等到协程执行完毕以后,再恢复执行后面的代码。

而对于 Deferred.await(),它的行为模式和 Job.join() 类似,只是它还会返回协程的执行结果。

另外,协程是结构化的并发,这是它的第二大优势。通过分析 Job 的源码,我们发现,一个 Job 可以拥有多个 ChildJob;对应的,协程也可拥有多个“子协程”。

那么结构化并发带来的最大优势就在于,我们可以实现只控制“父协程”,从而达到控制一堆子协程的目的。在前面的例子中,parentJob.join() 不仅仅只会等待它自身执行完毕,还会等待它内部的 job1、job2、job3 执行完毕。parentJob.cancel() 同理。