【翻译】kotlin协程核心库文档(四)—— 协程上下文和调度器

github原文地址

原创翻译,转载请保留或注明出处:https://www.jianshu.com/p/971f929f9bf5

协程上下文和调度器


协程总是在一些由kotlin标准库中定义的 CoroutineContext 类型值表示的上下文中执行。

协程上下文是一组不同的元素。主要元素是我们之前见过的协程的 Job ,以及本节讨论的调度器。

调度器和线程

协程上下文包括一个协程调度程序(参见 CoroutineDispatcher ),它确定对应协程的执行线程。协程调度器可以将协程的执行限制在一个特定的线程内,调度它到一个线程池,或者让它不受限制的运行。

所有协程构建器(如 launch 和 async )都接受可选的 CoroutineContext 参数,该参数可用于为新协程和其他上下文元素显式指定调度器。

尝试以下示例:

fun main(args: Array) = runBlocking {
    val jobs = arrayListOf()
    jobs += launch(Unconfined) { // not confined -- will work with  main thread
        println(" 'Unconfined': I'm working in thread ${Thread.currentThread().name}")
    }
    jobs += launch(coroutineContext) { // context of the parent, runBlocking coroutine
        println("'coroutineContext': I'm working in thread ${Thread.currentThread().name}")
    }
    jobs += launch(CommonPool) { // will get dispatched to [ForkJoinPool.commonPool](http://forkjoinpool.commonpool/) (or equivalent)
        println(" 'CommonPool': I'm working in thread ${Thread.currentThread().name}")
    }
    jobs += launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
        println(" 'newSTC': I'm working in thread ${Thread.currentThread().name}")
    }
    jobs.forEach { it.join() }
}

获取完整代码 here

输出如下(也许以不同的顺序):

'Unconfined': I'm working in thread main
'CommonPool': I'm working in thread [ForkJoinPool.commonPool-worker-1](http://forkjoinpool.commonpool-worker-1/)
'newSTC': I'm working in thread MyOwnThread
'coroutineContext': I'm working in thread main

我们在前面小节中使用的默认调度器是 DefaultDispatcher 表示的,它等同于当前实现中的 CommonPool 。所以launch { ... }等同于launch(DefaultDispatcher) { ... },等同于launch(CommonPool) { ... }

父 coroutineContext 和 Unconfined 上下文之间的区别将在稍后显示。

请注意一点,newSingleThreadContext 会创建一个新线程,这是一个非常昂贵的资源。在真实环境的应用程序中,它必须被释放掉,不再需要时,使用 close 函数,或者存在顶层变量中,并在整个应用程序中重用。

非受限 vs 受限 调度器

非受限协程调度器在调用者线程中启动协程,但仅限于第一个挂起点。在暂停之后,它将在挂起函数被调用的完全确定的线程中恢复。当协程不消耗CPU时间或者更新受限于特定线程的任何共享数据(如UI)时,非受限调度器是合适的。

另一方面,coroutineContext 属性(在任何协程中可用),都是对此特定协程上下文的引用。这样的话,父上下文可以被继承。runBlocking 协程的默认调度器,特别受限于调用者线程。因此继承它的总用是通过可预测的先进先出调度将执行限制在该线程中。

fun main(args: Array) = runBlocking {
    val jobs = arrayListOf()
    jobs += launch(Unconfined) { // not confined -- will work with main thread
        println(" 'Unconfined': I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println(" 'Unconfined': After delay in thread ${Thread.currentThread().name}")
    }
    jobs += launch(coroutineContext) { // context of the parent, runBlocking coroutine
        println("'coroutineContext': I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("'coroutineContext': After delay in thread ${Thread.currentThread().name}")
    }
    jobs.forEach { it.join() }
}

获取完整代码 here

输出如下:

'Unconfined': I'm working in thread main
'coroutineContext': I'm working in thread main
'Unconfined': After delay in thread kotlinx.coroutines.DefaultExecutor
'coroutineContext': After delay in thread main

因此,继承了(来自runBlocking {...}协程的)coroutineContext 的协程在主线程中继续执行,而非受限协程在 delay 函数正在使用的默认执行线程中恢复。

调试协程和线程

协程可以在一个线程上挂起,并在另一个具有非受限调度器或默认多线程调度器的线程上挂起。即便具有一个单线程调度器,弄清楚什么协程、何时、何处执行也是很困难的。调试具有线程的应用的常用方式是在没条日志语句中打印线程名称。这个特性得到日志框架的普遍支持。当使用协程时,只有线程名称不会提供更多的上下文,所以kotlinx.coroutines包含的调试工具让事情变得简单起来。

使用-Dkotlinx.coroutines.debugJVM参数运行以下代码:

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main(args: Array) = runBlocking {
    val a = async(coroutineContext) {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async(coroutineContext) {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")
}

获取完整代码 here

存在三个协程。一个runBlocking主协程(#1) , 以及两个计算延迟值的协程——a(#2) 和b (#3)。 它们都运行在runBlocking的上下文中,并且限制在主线程中。这段代码输出如下:

[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42

log函数在方括号中打印线程的名称,你可以看到它是main线程,但当前正在执行的协程的标识符附加到了后面。在打开调试模式时,此标识符会连续地分配给所有创建的协程。

你可以在 newCoroutineContext 函数的文档中阅读有关调试工具的更多信息。

在线程间跳跃

使用-Dkotlinx.coroutines.debugJVM参数运行以下代码:

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main(args: Array) {
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")
            }
        }
    }
}

获取完整代码 here

这里演示了几种新技术。一个是使用具有明确指定上下文的 runBlocking ,另一个是使用 withContext 函数改变协程的上下文,同时仍旧停留在相同的协程中,如下面的输出中所示:

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1

请注意,该示例还使用了Kotlin标准库中的use函数来释放使用了 newSingleThreadContext 创建的线程,当它们不再被需要时。

上下文中的Job

协程的 Job 是其上下文的一部分,协程可以使用coroutineContext[Job]表达式从它自己的上下文中拿到 Job :

fun main(args: Array) = runBlocking {
    println("My job is ${coroutineContext[Job]}")
}

获取完整代码 here

当运行在 debug mode 中,输出如下:

My job is "coroutine#1":BlockingCoroutine{Active}@6d311334

所以在 CoroutineScope 中的 isActive 只是coroutineContext[Job]?.isActive == true的一个方便的快捷方式。

子协程

当一个协程的 coroutineContext 被用来启动另一个协程,新协程的 Job 成为父协程的一个子Job,当父协程被取消时,所有它的子协程也会被递归取消。

fun main(args: Array) = runBlocking {
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        // it spawns two other jobs, one with its separate context
        val job1 = launch {
            println("job1: I have my own context and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")
        }
        // and the other inherits the parent context
        val job2 = launch(coroutineContext) {
            delay(100)
            println("job2: I am a child of the request coroutine")
            delay(1000)
            println("job2: I will not execute this line if my parent request is cancelled")
        }
        // request completes when both its sub-jobs complete:
        job1.join()
        job2.join()
    }
    delay(500)
    request.cancel() // cancel processing of the request
    delay(1000) // delay a second to see what happens
    println("main: Who has survived request cancellation?")
}

获取完整代码 here

这段代码输出如下:

job1: I have my own context and execute independently!
job2: I am a child of the request coroutine
job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?

结合上下文

协程上下文可以通过 + 操作符结合。右侧的上下文替换了左侧上下文的相关条目。例如,一个 父协程的 Job 可以被继承,同时替换它的调度器:

fun main(args: Array) = runBlocking {
    // start a coroutine to process some kind of incoming request
    val request = launch(coroutineContext) { // use the context of `runBlocking`
        // spawns CPU-intensive child job in CommonPool !!!
        val job = launch(coroutineContext + CommonPool) {
            println("job: I am a child of the request coroutine, but with a different dispatcher")
            delay(1000)
            println("job: I will not execute this line if my parent request is cancelled")
        }
        job.join() // request completes when its sub-job completes
    }
    delay(500)
    request.cancel() // cancel processing of the request
    delay(1000) // delay a second to see what happens
    println("main: Who has survived request cancellation?")
}

获取完整代码 here

这段代码的预期输出如下:

job: I am a child of the request coroutine, but with a different dispatcher
main: Who has survived request cancellation?

父协程的职责

一个父协程总是等待其全部子协程的完成。而且并不需要显示地追踪它启动的所有子协程,也不必使用 Job.join 在最后等待它们:

fun main(args: Array) = runBlocking {
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        repeat(3) { i -> // launch a few children jobs
            launch(coroutineContext) {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // wait for completion of the request, including all its children
    println("Now processing of the request is complete")
}

获取完整代码 here

结果将是:

request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete

为调试命名协程

当协程经常产生日志时,自动分配的id是可以接受的,你只需要关联这些来自同一协程的日志记录。然而当协程与特定请求的处理或特定的后台任务相关时,最好为其明确地命名以方便调试。CoroutineName 上下文与线程名具有相同的功能。当debugging mode 开启后,它将显示在执行此协程的线程名称中。

以下示例示范了此概念:

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main(args: Array) = runBlocking(CoroutineName("main")) {
    log("Started main coroutine")
    // run two background value computations
    val v1 = async(CoroutineName("v1coroutine")) {
        delay(500)
        log("Computing v1")
        252
    }
    val v2 = async(CoroutineName("v2coroutine")) {
        delay(1000)
        log("Computing v2")
        6
    }
    log("The answer for v1 / v2 = ${v1.await() / v2.await()}")
}

获取完整代码 here

带有 -Dkotlinx.coroutines.debug JVM 选项的输出类似于:

[main @main#1] Started main coroutine
[[ForkJoinPool.commonPool-worker-1](http://forkjoinpool.commonpool-worker-1/) @v1coroutine#2] Computing v1
[[ForkJoinPool.commonPool-worker-2](http://forkjoinpool.commonpool-worker-2/) @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 / v2 = 42

明确地取消指定job

让我们把上下文、父子和jobs的认知合起来。假设一个应用程序,它拥有一个具有生命周期的对象,但该对象不是一个协程。例如,我们正在编写一个Android应用,并在Android activity的上下文中启动了各种协程用来执行异步操作,比如获取、更新数据和执行动画等等。当activity被销毁时所有这些协程必须被取消,以避免内存泄漏。

我们可以通过创建与activity生命周期相关联的 Job 的实例来管理协程的生命周期。一个job实例可以通过 Job() 工厂函数创建,如以下示例所示。为方便起见,我们可以编写 launch(coroutineContext, parent = job)来明确表示正在使用父job这一事实,而不是使用 launch(coroutineContext + job)表达式。

现在,Job.cancel 的单个调用取消了我们启动的所有子协程。此外,Job.join 等待所有子协程的完成,所以我们也可以在这个例子中使用 cancelAndJoin:

fun main(args: Array) = runBlocking {
    val job = Job() // create a job object to manage our lifecycle
    // now launch ten coroutines for a demo, each working for a different time
    val coroutines = List(10) { i ->
        // they are all children of our job object
        launch(coroutineContext, parent = job) { // we use the context of main runBlocking thread, but with our parent job
            delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
            println("Coroutine $i is done")
        }
    }
    println("Launched ${coroutines.size} coroutines")
    delay(500L) // delay for half a second
    println("Cancelling the job!")
    job.cancelAndJoin() // cancel all our coroutines and wait for all of them to complete
}

获取完整代码 here

这个示例的输出如下:

Launched 10 coroutines
Coroutine 0 is done
Coroutine 1 is done
Cancelling the job!

正如你所见,只有前三个协程打印了一条消息,而其他协程被一次job.cancelAndJoin()调用取消掉了。所以在我们假设的Android应用中,我们需要做的是当activity被创建时同时创建一个父job对象,将它用于子协程,并在activity销毁时取消它。我们无法在Android的生命周期中join 它们,因为它是同步的。但是这种join 的能力在构建后端服务时是很有用的,用来确保有限资源的使用。

你可能感兴趣的:(【翻译】kotlin协程核心库文档(四)—— 协程上下文和调度器)