协同程序总是在某个上下文中执行,该上下文由在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是其上下文的一部分。 协程可以使用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的知识放在一起讲述。 假设我们的应用程序有一个具有生命周期的对象,但该对象不是协程。 例如,我们正在编写一个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接口的文档。