协程上下文和协程调度器(官方文档)
目录
2、非限定和限定调度器(Unconfined vs confined dispatcher)
9、组合协程上下文元素(Combining context elements)
协程通常运行于协程标准库中定义的CoroutineContext所代表协程上下文中。
协程上下文其实是一系列的不同元素集合,其中最主要的元素就是之前我们用过的协程Job,还有这里将要介绍的协程调度器。
1、协程调度器和线程
协程上下文中包含一个协程调度器(CoroutineDispatcher)可以用来调度协程的执行,它将决定协程是运行于线程中还是线程池中。协程调度器可以限定一个协程运行在特定的线程中、调度到线程池中运行或者无限制的运行在线程之间。
所有的协程构建器就像launch和async一样接收一个可选参数CoroutineContext ,这个参数可以用来指定协程调度器以及其他的协程上下文信息。
你可以运行下面的程序试试:
package com.cool.cleaner.test
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
launch {//context of the parent, main runBlocking coroutine
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {//work with main thread
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {//will get dispatched to DefaultDispatcher
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) {//will get its own new thread
println("newSingleThreadContext : I'm working in thread ${Thread.currentThread().name}")
}
}
输出如下(可能你运行跑出来的顺序和这里不太一样):
Unconfined : I'm working in thread main @coroutine#3
Default : I'm working in thread DefaultDispatcher-worker-2 @coroutine#4
main runBlocking : I'm working in thread main @coroutine#2
newSingleThreadContext : I'm working in thread MyOwnThread @coroutine#5
Process finished with exit code 0
当调用launch { ... }的无参版本时,它将会从启动它的CoroutineScope 中继承协程上下文,当然也继承了协程调度器;在上面的例子中,它从运行于主线程的runBlocking
中继承了协程上下文。
Dispatchers.Unconfined 是一种特殊的调度器而且看起来他也是运行于main线程中,但是它其实是另一种不同的机制,接下来会介绍到。
在GlobalScope中启动的协程默认使用的协程调度器是Dispatchers.Default,并且使用共享的后台线程池,因此launch(Dispatchers.Default) { ... }
和 GlobalScope.launch { ... }这两种启动协程的方式使用的是相同的协程调度器
Dispatchers.Default。
newSingleThreadContext 则创建了一个给协程运行的独立线程,用一个专门的线程来运行协程是非常耗资源的;在实际的应用中,当你不再使用的时候你必须调用close函数关闭它或者保存在一个顶级变量中以便在整个应用中可以复用它。
2、非限定和限定调度器(Unconfined vs confined dispatcher)
协程调度器Dispatchers.Unconfined 会在调用它的线程中启动一个协程,但是仅仅只是在协程中调用第一个suspend函数之前,在suspend函数调用之后协程可能调度到其他线程中运行,这取决于你调用的suspend函数的实现(意思就是调用suspend函数后协程可能会运行于不同的线程中)。这种非限定的调度器适合于调度那种不大量消耗CPU时间(非CPU密集型的任务)并且也不更新特定线程(比如UI线程)中的共享数据的协程。
另一方面,调度器默认继承于外层的CoroutineScope ,对于runBlocking 协程来说默认的调度器受到调用runBlocking 的线程的限制,,,因此这种情况下的继承会限定线程执行于调用runBlocking 的线程中,代码如下:
package com.cool.cleaner.test
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
launch(Dispatchers.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}")
}
launch {//context of the parent, main runBlocking coroutine
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 @coroutine#2
main runBlocking : I'm working in thread main @coroutine#3
Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor @coroutine#2
main runBlocking : After delay in thread main @coroutine#3
Process finished with exit code 0
你可以看到从runBlocking {...}中继承了协程上下文的协程运行于main线程中,而没有限定的那个则运行在delay函数使用的默认线程中。
非限定调度器(Dispatchers.Unconfined)是一种高级机制,在极少的情况下是非常有用的,比如:有一些操作需要马上执行,但之后协程的调度运行并不是那么重要或者会产生一些不希望的副作用,此时就可以使用它。在一般情况下你是不需要使用它的。
3、调式协程和线程
协程可以在一个线程挂起并在另一个线程恢复,如果你不适用特殊的工具调式的话,即使是单线程调度器也很难明白协程在干什么、运行到哪里了。
3.1、使用IDEA调式
kotlin插件中的Coroutine Debugger可以简化在IntelliJ IDEA中调式协程。(没有使用IDEA,所以直接翻译了)
调式器只对
kotlinx-coroutines-core
1.3.8之后的版本有效
在调式窗口中有一个Coroutines的tab,在这个tab中你可以看到当前处于运行状态(running)的和挂起的协程(suspended),协程是按照调度他们的调度器来组织的。如下图:
(这是官网图片:好像我并么有看到相应的tab啊)
使用协程调式器,你可以:
- 查看每一个协程的状态。
- 请查看正在运行和挂起的协程的本地和捕获变量的值。
- 查看创建协程的所有栈、调用栈,栈中包含了所有变量的值。
- 导出一个包含所有协程及相应栈的一个报告,你可以在Coroutines这个tab中右键点击然后选择Get Coroutines Dump。
你只需要在代码中设置断点然后在debug模式下运行程序即可开始调式应用,你也可以点击这里学习有关协程调式的更多技巧。
3.2、使用Log调式
另一种调式线程的方法是在log中打印出线程的名字,这种方法在log系统中都是普遍支持的;在使用协程的时候,仅仅只是线程名字并不会包含多少关于线程上下文的信息,因此库kotlinx.coroutines中包含了一些工具使得log调式更容易些。
使用JVM参数-Dkotlinx.coroutines.debug运行下面的程序:
package com.cool.cleaner.test
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
fun log(msg: String): Unit {
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()}")
}
这里有三个协程, 位于runBlocking中的主协程
main coroutine (#1)以及计算a值和b值的另外两个协程。他们都在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
Process finished with exit code 0
log函数会把线程的名字打印在方括号里面,打印出的名字是main + @coroutine#xxx,当打开调式模式的时候#xxx这部分的取值是连续的。
当使用选项
-ea
运行JVM的时候也会打开调试模式,你可以在该文档中查看关于属性DEBUG_PROPERTY_NAME 的调试工具。
4、在线程之间切换
使用JVM选项-Dkotlinx.coroutines.debug运行下面的程序:
package com.cool.cleaner.test
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
fun log(msg: String): Unit {
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显示指定特定的协程上下文,另一个是使用withContex切换协程上下文,下面是程序输出:
[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1
Process finished with exit code 0
这个例子中还使用了标准库中的use函数释放由newSingleThreadContext 创建的不再使用的线程。
5、协程上下文的Job
线程的job对象是协程上下文的一部分,你可以使用表达式coroutineContext[Job]获取它,如下代码所示:
package com.cool.cleaner.test
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
println("My job is ${coroutineContext[Job]}")
}
在调式模式下的输出就像下面这样的:
My job is "coroutine#1":BlockingCoroutine{Active}@6477463f
Process finished with exit code 0
请注意,使用 CoroutineScope的isActive其实是表达式coroutineContext[Job]?.isActive == true的一种简写。
6、子协程
当一个协程在另一个协程的作用域CoroutineScope 中启动的时候,它就会通过CoroutineScope.coroutineContext继承父协程的协程上下文,同时子协程的job会变成父协程job的孩子(job在父子协程之间也有父子关系),当父协程取消的时候所有子协程也会递归的取消。
然而,当使用全局作用域GlobalScope启动一个新协程的时候,这个新协程是没有父协程的,因此它不会与启动它的协程作用域绑定(也就是没有父子关系),并且是独立运行的。如下代码所示:
package com.cool.cleaner.test
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
val request = launch {
GlobalScope.launch {//独立运行的,不受父协程取消的限制
println("job1: I run in GlobaScope 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 GlobaScope 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 ?
Process finished with exit code 0
7、父协程的责任
父协程通常会等待子协程执行完成才会退出,它不需要显示的跟踪它启动的所有子协程,也不需使用Job.join等待子协程的结束,反正一切都是自动的,只有子协程执行完成了父协程才会退出,代码如下:
package com.cool.cleaner.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
val request = launch {
repeat(3) { index ->
launch {
delay((index + 1) * 200L)
println("Coroutine $index 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
Process finished with exit code 0
8、调式状态下的协程名字
当协程经常记录打印日志时,自动分配协程id也不错,你查看日志的时候也只需要把来自同一个协程的日志关联在一起就好了;然而当一个协程需要执行特殊的请求或者一些特殊的后台任务的时候,为了调式方便最好是给协程取一个有意义的名字。线程的名字一样,上下文元素CoroutineName 可以达到你的目的;当调试模式打开的时候它就会关联到运行此协程的线程名字中。
下面的代码证明了刚刚的理论:
package com.cool.cleaner.test
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
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(100)
log("Computing v2")
6
}
log("The answer for v1 / v2 = ${v1.await() / v2.await()}")
}
使用JVM选项-Dkotlinx.coroutines.debug运行上面的程序,输出如下:
[main @main#1] Started main coroutine
[main @v2coroutine#3] Computing v2
[main @v1coroutine#2] Computing v1
[main @main#1] The answer for v1 / v2 = 42
Process finished with exit code 0
9、组合协程上下文元素(Combining context elements)
有时候我们需要为一个协程指定多个上下文元素,此时我们可以使用操作符"+",举个例子:我们可以在启动一个协程的时候同时为它指定协程调度器和名字:
package com.cool.cleaner.test
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
}
使用JVM参数-Dkotlinx.coroutines.debug运行这段代码的输出结果如下:
I'm working in thread DefaultDispatcher-worker-1 @test#2
Process finished with exit code 0
10、协程作用域(Coroutine scope)
这里举个例子,这个例子将会使用到之前关于上下文、job、父子协程的知识;假如我们的应用程序有个具有生命周期的对象,但是这个对象并不是一个协程;比如我们在写Android程序的时候会在activity中启动不同的协程取获取数据、更新数据,为了避免内存泄漏在activity页面关闭的时候我们就需要取消所有已经启动的协程,你当然可以自己追踪协程上下文和job的引用并在适当的时候结束他们,但是协程库kotlinx.coroutines为我们提供了一个 CoroutineScope的抽象,你应该已经熟悉协程作用域了因为所有的协程构建器都是 CoroutineScope的一个扩展。
我们可以创建一个绑定到activity生命周期的协程作用域 CoroutineScope,然后用它来管理协程的生命周期,一个协程作用域可以使用工厂函数CoroutineScope() 或 MainScope() 来创建,前者创建一个普通的作用域,后者创建一个针对UI线程的作用域并且使用Dispatchers.Main 作为默认的协程调度器,相关模拟代码如下:
class Activity {
private val mainScope = MainScope()
fun destroy(): Unit {
mainScope.cancel()
}
}
现在我们可以在这个Activity中使用定义的mainScope启动协程了,这里我们启动了10个协程然后延时不同的时间,代码如下:
class Activity {
private val mainScope = MainScope()
fun destroy(): Unit {
mainScope.cancel()
}
fun doSomething(): Unit {//在mainScope中启动多个协程
repeat(10) { index ->
mainScope.launch {
delay((index + 1) * 200L)
println("Coroutine $index is done")
}
}
}
}
在主函数里面,我们创建activity对象,调用doSomething
函数,然后500ms后销毁activity,这将会销毁所有从doSomething
中启动的协程,你可以看到activity对象销毁后就没有更多的输出了,即使我们等待时间长一点也是一样,最终代码如下:
package com.cool.cleaner.test
import kotlinx.coroutines.*
class Activity {
private val mainScope = MainScope()
fun destroy(): Unit {
mainScope.cancel()
}
fun doSomething(): Unit {//在mainScope中启动多个协程
repeat(10) { index ->
mainScope.launch {
delay((index + 1) * 200L)
println("Coroutine $index is done")
}
}
}
}
fun main() = runBlocking<Unit> {
val activity = Activity()
activity.doSomething()
println("Launched coroutines")
delay(500)
println("Destroying activity")
activity.destroy()
delay(1000)
}
下面是输出:
Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!
如你所见,只有前两个协程输出了日志,其他的都被Activity.destroy()中的job.cancel()调用取消了。
请注意:我在Android Studio上的测试结果和官网的有出入,你自己也可以试试喔。
11、线程本地存储(Thread-local data)
有时候在协程之间传递一些Thread-local数据会比较有用,然而因为协程并不会绑定到任何一个协程,所以如果你想自己实现的话会比较麻烦。
对于ThreadLocal来说,扩展函数
asContextElement 可以帮助你解决问题,它会创建一个额外的上下文元素来保存ThreadLocal
的值,然后在协程切换上下文的时候恢复它,如下代码所示:
package com.cool.cleaner.test
import kotlinx.coroutines.*
val threadLoca = ThreadLocal<String?>()
fun main() = runBlocking<Unit> {
threadLoca.set("main")
println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLoca.get()}'")
val job = launch(Dispatchers.Default + threadLoca.asContextElement(value = "launch")) {
println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLoca.get()}'")
yield()
println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLoca.get()}'")
}
job.join()
println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLoca.get()}'")
}
在这个例子中,我们使用调度器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-1 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Process finished with exit code 0
假如你忘记了设置额外的上下文元素,而运行协程的线程又是不同的,那你从协程从取出来的线程本地存储值将会是不可意料的(就是可能每次取出来的值都不一样)。为了避免这种情况,推荐使用ensurePresent和不当使用的时候启用快速失败机制。
在kotlinx.coroutines库中,ThreadLocal得到了首选支持并且可以使用库提供的任何原语;它的一个不足就是:当一个thread-local数据改变的时候,新值不会传递给协程的调用者(因为一个上下文元素无法跟踪所有ThreadLocal对象的访问),而新值会在下一次挂起的时候丢失。在协程中可以使用函数 withContext 更新thread-local的值,可以在asContextElement这了解更多的信息。
还有另一种保存thread-local数据的方式就是把它存放在一个包装类中比如class Counter(var i: Int),然后把包装类当作thread-local来存储,然而这种情况下你就要完全负责包装类对象的同步访问了(自己进行同步,避免线程竞争)。
对于一些高级应用,比如与logging MDC集成、事务性上下文或者其他在内部使用thread-locals传递数据的第三方库,你可以看看关于ThreadContextElement 接口的文档以便了解哪些接口是应该要实现的。