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

github原文地址

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

协程上下文和调度器


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

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

调度器和线程

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

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

尝试以下示例:

fun main(args: Array<String>) = runBlocking<Unit> {
    val jobs = arrayListOf<Job>()
    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) { ... }

coroutineContextUnconfined 上下文之间的区别将在稍后显示。

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

非受限 vs 受限 调度器

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

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

fun main(args: Array<String>) = runBlocking<Unit> {
    val jobs = arrayListOf<Job>()
    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<String>) = runBlocking<Unit> {
    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<String>) {
    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<String>) = runBlocking<Unit> {
    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<String>) = runBlocking<Unit> {
    // 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<String>) = runBlocking<Unit> {
    // 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<String>) = runBlocking<Unit> {
    // 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<String>) = 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<String>) = runBlocking<Unit> {
    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 的能力在构建后端服务时是很有用的,用来确保有限资源的使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272