kotlin coroutine文档:协程上下文和调度器

协程上下文和调度器

协同程序总是在某个上下文中执行,该上下文由在Kotlin标准库中定义的CoroutineContext类型的值表示。

协程上下文是一组各种元素的集合。 主要元素是协程的Job及其调度器,前者我们见过,后者本节将对其进行介绍。

调度器和线程

协程上下文包括一个协程调度器(请参阅CoroutineDispatcher),它确定相应的协程在哪个或者哪些线程里执行。 协程调度器可以将协程执行限制在特定线程,将其分派给线程池,或让它无限制地运行。

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

请尝试以下示例:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//例子开始
    launch { // 父协程的上下文,主runBlocking协程
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // 非受限 -- 将和主线程中运行
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // 将调度到DefaultDispatcher
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // 将有自己的线程
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
//例子结束    
}

在这里获取完整代码

它产生以下输出(可能以不同的顺序):

Unconfined            : I'm working in thread main
Default               : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking      : I'm working in thread main

当使用不带参数的launch{…}时,它会从正在启动的CoroutineScope中继承上下文(以及调度器)。 在这种情况下,它继承了在主线程中运行的主runBlocking协程的上下文。

Dispatchers.Unconfined是一个特殊的调度程序,它似乎也在主线程中运行,但它实际上是一种不同的机制,稍后会解释。

当协程GlobalScope中启动,它使用的默认调度器由Dispatchers.Default表示,并使用共享的后台线程池,因此launch(Dispatchers.Default) { … }使用了和GlobalScope.launch { … }相同的调度器。

newSingleThreadContext为协程运行创建一个新线程。 专用的线程是一种非常昂贵的资源。 在实际应用程序中,它必须在不再需要时使用close函数释放,或者存储在顶层变量中并在整个应用程序中重用。

非受限和受限调度器

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

另一方面,默认情况下,它继承外部CoroutineScope的调度器。 特别地,runBlocking协程的默认调度程序限定于调用线程,因此继承它有这样的效果:执行限定在此线程中,具有可预测的FIFO调度。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//例子开始
    launch(Dispatchers.Unconfined) { // 非受限 -- 将和主线程中运行
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // 父协程的上下文,主runBlocking协程
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
    }
//例子结束
}

在这里获取完整代码

它产生以下输出:

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

因此,继承了runBlocking {…}上下文的协程继续在主线程中执行,而非受限的协程已在delay函数正在使用的默认执行程序线程中恢复。

非受限调度器是一种高级的机制,在某些极端情况下可能会有所帮助:稍后执行协程的调度不再需要或产生不良副作用,因为必须立即执行协程中的某些操作。 非受限的调度器不应在一般代码中使用。

协程和线程的调试

协程可以在一个线程上挂起并在另一个线程上继续。 即使使用单线程调度器,也可能很难弄清楚协程在何时何地正在做什么。 使用线程应用程序的调试的常用方法是,在日志文件中打印每个日志语句的线程名称。 日志框架普遍支持此功能。 使用协同程序时,单独的线程名称不会给出上下文的很多信息,因此kotlinx.coroutines包含了调试工具以使其更容易。

用 -Dkotlinx.coroutines.debug JVM选项,运行如下代码:

import kotlinx.coroutines.*

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

fun main() = runBlocking<Unit> {
//例子开始
    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")
//例子结束    
}

在这里获取完整代码

这里有三个协程。 主协程(#1) – runBlocking是第一个,其他两个协程计算延时值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函数在方括号中打印线程的名称,您可以看到它是主线程,但是当前正在执行的协程的标识符被附加到它。 打开调试模式时,会将此标识符连续分配给所有已创建的协同程序。

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

在多个线程间跳转

用 -Dkotlinx.coroutines.debug JVM选项,运行如下代码(参考debug):

import kotlinx.coroutines.*

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

fun main() {
//例子开始
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")
            }
        }
    }
//例子结束    
}

在这里获取完整代码

它展示了几种新技巧。 一个是使用带有显式指定上下文的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]表达式从其自己的上下文中获取它:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//例子开始
    println("My job is ${coroutineContext[Job]}")
//例子结束    
}

在这里获取完整代码

在调试模式下运行时会产生类似的东西:

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

注意,CoroutineScope中的isActive只是coroutineContext[Job]?.isActive == true 的一个方便快捷方式。

协程的子线程

当在另一个协程的CoroutineScope中启动协程时,它通过CoroutineScope.coroutineContext继承其上下文,并且新协同程序的Job成为父协程工作的子项。 当父协程被取消时,也会递归地取消它的所有子节点。

但是,当GlobalScope用于启动协程时,它没有绑定到从其启动的作用域,并且独立运行。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//例子开始
    // 启动协程来处理某种传入请求
    val request = launch {
        //它产生了另外两个Job,一个是GlobalScope
        GlobalScope.launch {
            println("job1: I run in GlobalScope and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")
        }
        // 另外一个继承了父上下文
        launch {
            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")
        }
    }
    delay(500)
    request.cancel() // 取消请求的处理
    delay(1000) // 延迟一秒看看发生什么
    println("main: Who has survived request cancellation?")
//例子结束
}

在这里获取完整代码

这段代码的结果是:

job1: I run in GlobalScope 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.join在最后面等待它们:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//例子开始
    // 启动协程来处理某种传入请求
    val request = launch {
        repeat(3) { i -> // 启动一些子job
            launch  {
                delay((i + 1) * 200L) //不同的延时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() // 等待请求结束,包括所有子协程
    println("Now processing of the request is complete")
//例子结束
}

在这里获取完整代码

结果如下:

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这个上下文元素与线程名称具有相同的功能。 当调试模式打开时,它将显示在执行此协程的线程名称中。

以下示例展示了此概念:

import kotlinx.coroutines.*

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

fun main() = runBlocking(CoroutineName("main")) {
//例子开始
    log("Started main coroutine")
    // 运行两个后台值计算
    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()}")
//例子结束    
}

在这里获取完整代码

用 -Dkotlinx.coroutines.debug这个JVM选项,产生的结果类似于:

[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 / v2 = 42

组合上下文元素

有时我们需要为协程上下文定义多个元素。 我们可以使用+运算符。 例如,我们可以同时使用显式指定的调度器和显式指定的名称启动协程:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//例子开始
    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }
//例子结束    
}

在这里获取完整代码

用 -Dkotlinx.coroutines.debug这个JVM选项,产生的结果如下:

I'm working in thread DefaultDispatcher-worker-1 @test#2

通过显式的job取消

让我们将关于上下文,子协程和job的知识放在一起讲述。 假设我们的应用程序有一个具有生命周期的对象,但该对象不是协程。 例如,我们正在编写一个Android应用程序,并在Android的activity的上下文中启动各种协程,执行异步操作以获取和更新数据,执行动画等。所有这些协程必须在活动被销毁时取消,以避免内存泄漏。

我们创建一个与activity的生命周期绑定的Job实例,通过它管理协程的生命周期。 创建activity时,使用Job()工厂函数创建作业实例,并在销毁activity时取消job实例,如下所示:

class Activity : CoroutineScope {
    lateinit var job: Job

    fun create() {
        job = Job()
    }

    fun destroy() {
        job.cancel()
    }
    // 未完待续 ...

我们也在Actvity类中实现了CoroutineScope接口。 我们只需要为其CoroutineScope.coroutineContext属性提供一个覆写,以指定在其作用域内启动的协程上下文。 我们将所需的调度器(我们在此示例中使用Dispatchers.Default)和job组合在一起:

    // Activity类继续
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default + job
    // 未完待续 ...

现在,我们可以在此Activity的作用域内启动协程,而无需显式指定其上下文。 对于演示,我们启动了十个协程,这些协程延迟了不同的时间:

    // Activity类继续
    fun doSomething() {
        // 为了演示,启动十个协程,每个协程运行不同的时间
        repeat(10) { i ->
            launch {
                delay((i + 1) * 200L) // 不同延时200ms, 400ms, ... 等待
                println("Coroutine $i is done")
            }
        }
    }
} // Activity类结束

在我们的主函数中,我们创建了activity,调用我们的doSomething测试函数,并在500ms后销毁activity。 这取消了所有已启动的协程,如果我们等待一会儿,注意到它不再打印到屏幕上,我们从这一点可以确认:

import kotlin.coroutines.*
import kotlinx.coroutines.*

class Activity : CoroutineScope {
    lateinit var job: Job

    fun create() {
        job = Job()
    }

    fun destroy() {
        job.cancel()
    }
    // 未完待续 ...

    // Activity类继续
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default + job
    // 未完待续 ...

    // Activity类继续
    fun doSomething() {
        // 为了演示,启动十个协程,每个协程运行不同的时间
        repeat(10) { i ->
            launch {
                delay((i + 1) * 200L) // 不同延时200ms, 400ms, ... 等待
                println("Coroutine $i is done")
            }
        }
    }
} // Activity类结束

fun main() = runBlocking<Unit> {
//例子开始
    val activity = Activity()
    activity.create() // 创建activity
    activity.doSomething() // 运行测试函数
    println("Launched coroutines")
    delay(500L) // 延迟半秒
    println("Destroying activity!")
    activity.destroy() // 取消所有协程
    delay(1000) // 目视确认它们不起作用
//例子结束    
}

在这里获取完整代码

这个例子的结果如下:

Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!

正如您所看到的,只有前两个协同程序打印了一条消息,而其他协程都被Activity.destroy()中的job.cancel() 单一调用取消。

线程本地数据

有时候能够传递一些线程本地数据是很方便的,但是,对于没有绑定到任何特定线程的协程,很难在不编写大量样板代码的情况下手动实现它。

ThreadLocal,asContextElement扩展函数就是拯救这种情况。 它创建了一个额外的上下文元素,保留了给定的ThreadLocal值,并在每次协程切换其上下文时恢复。

在实际中很容易证明:

import kotlinx.coroutines.*

val threadLocal = ThreadLocal<String?>() // 声明线程本地变量
fun main() = runBlocking<Unit> {
//sampleStart
    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
       println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
        yield()
        println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    }
    job.join()
    println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
//sampleEnd    
}

在这里获取完整代码

在这个例子中,我们使用Dispatchers.Default在后台线程池中启动新协程,因此它可以在与线程池不同的线程上工作,但它仍然具有线程本地变量的值,我们使用threadLocal.asContextElement(value = “launch”)指定,无论协程执行什么线程。 因此,输出(使用调试)是:

Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'

ThreadLocal具有一等的支持,可以与kotlinx.coroutines提供的任何原始类型一起使用。 它有一个关键限制:当线程本地改变时,新值不会传播到协程调用者(因为上下文元素无法跟踪所有ThreadLocal对象访问),并且更新的值在下次挂起时丢失。 使用withContext更新协程中的线程本地值,有关更多详细信息请参阅asContextElement。

或者,可以将值存储在像Counter(var i: Int)这样类的可变箱子中,该类又存储在线程本地变量中。 但是,在这种情况下,您完全有责任同步在此可变箱子中该变量的可能并发修改。

对于高级用法,例如,与日志记录MDC,事务上下文或任何其他库(内部使用线程本地传递数据)的集成,请参阅应实现的ThreadContextElement接口的文档。

你可能感兴趣的:(Kotlin,Coroutine)