Kotlin协程上下文与调度器
协程总是运行在⼀些以 CoroutineContext 类型为代表的上下文中,它们被定义在了 Kotlin 的标准库里。协程上下文是各种元素的集合,其中主要元素是协程中的Job(前面提到lanch和async都会返回一个job)
一、调度器与线程
协程上下文包含⼀个协程调度器(参见 CoroutineDispatcher)它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在⼀个特定的线程执行,或将它分派到⼀个线程池,亦或是让它不受限地运行。
所有的协程构建器诸如 launch 和 async 接收⼀个可选的 CoroutineContext 参数,它可以被用来显式的为⼀个新协程或其它上下文元素指定⼀个调度器。
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) { // 将会获取默认调度器
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
main runBlocking : I'm working in thread main
newSingleThreadContext: I'm working in thread MyOwnThread
- 调用 launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下文(以及调度器)。
- Dispatchers.Unconfined是⼀个特殊的调度器且似乎也运行在 main 线程中,但实际上,它是⼀种不同的机制。
- 当协程在 GlobalScope 中启动时,使用的是由 Dispatchers.Default 代表的默认调度器。默认调度器使用共享的后台线程池。launch(Dispatchers.Default) { …… } 与 GlobalScope.launch { …… } 使用相同的调度器。
- newSingleThreadContext 为协程的运行启动了⼀个线程。⼀个专用的线程是⼀种非常昂贵的资源。在真实的应用程序中两者都必须被释放,当不再需要的时候,使用 close 函数,或存储在⼀个顶层变量中使它在整个应用程序中被重用。
二、非受限调度器vs受限调度器
Dispatchers.Unconfined 协程调度器在调用它的线程启动了⼀个协程,但它仅仅只是运行到第⼀个挂起点。挂起后,它恢复线程中的协程,而这完全由被调用的挂起函数来决定。非受限的调度器非常适用于执行不消耗 CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。 另⼀方面,该调度器默认继承了外部的 CoroutineScope。runBlocking 协程的默认调度器,特别是,当它被限制在了调用者线程时,继承自它将会有效地限制协程在该线程运行并且具有可预测的 FIFO 调度。
所以该协程的上下文继承自 runBlocking {...} 协程并在 main 线程中运行,当 delay 函数调用的时候,非受限的那个协程在默认的执行者线程中恢复执行
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
三、调试协程与线程
协程可以在⼀个线程上挂起并在其它线程上恢复。如果没有特殊工具,甚至对于⼀个单线程的调度器也是难以弄清楚协程在何时何地正在做什么事情。
1.用IDEA调式
image.png2.用日志调式
让线程在每⼀个日志文件的日志声明中打印线程的名字,用log函数就行
在不同线程间跳转
协程可在不同的线程中跳转。使用 runBlocking 来显式指定了⼀个上下文,并且另⼀个使用 withContext 函数来改变协程的上下文,而仍然驻留在相同的协程中。不需要某个在 newSingleThreadContext 中创建的线程的时候,使用 use 函数来释放该线程。
fun main() = runBlocking {
newSingleThreadContext("Ctx1").use { ctx1 ->
newSingleThreadContext("Ctx2").use { ctx2 ->
runBlocking(ctx1) {
log("Started in ctx1")
withContext(ctx2) {
log("Working in ctx2")
}
log("Back to ctx1")
}
}
}
}
输出:
[Ctx1] Started in ctx1
[Ctx2] Working in ctx2
[Ctx1] Back to ctx1
四、上下文中的作业
协程的 Job 是上下文的⼀部分,并且可以使用 coroutineContext [Job] 表达式在上下文中检索它。CoroutineScope 中的 isActive 只是 coroutineContext[Job]?.isActive == true 的⼀种方便的快捷方式。(在协程取消时有提到,使计算协程可退出)
五、子协程
当⼀个协程被其它协程在 CoroutineScope 中启动的时候,它将通过 CoroutineScope.coroutineContext 来承袭上下文,并且这个新协程的 Job 将会成为父协程作业的子作业。当⼀个父协程被取消的时候,所有它的子协程也会被递归的取消。
当使用 GlobalScope 来启动⼀个协程时,则新协程的作业没有父作业。因此它与这个启动的作用域无关且独立运作。
fun main() = runBlocking {
val request = launch {
GlobalScope.launch {
println("job0: I run in GlobalScope and execute independently!")
delay(1000)
println("job0: GlobalScope is not affected by cancellation of the request")
}
launch(Job()) {
println("job1: I run in my own Job and execute independently!")
delay(1000)
println("job1: own Job is not affected by cancellation of the request")
}
launch {
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.cancelAndJoin()
delay(1000)
println("main: Who has survived request cancellation?")
}
输出:
job0: I run in GlobalScope and execute independently!
job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
job1: own Job is not affected by cancellation of the request
job0: GlobalScope is not affected by cancellation of the request
main: Who has survived request cancellation?
六、父协程的职责
⼀个父协程总是等待所有的子协程执行结束。父协程并不显式的跟踪所有子协程的启动,并且子协程不必使用 Job.join
七、命名协程以用于调试
当协程经常打印日志并且你只需要关联来⾃同⼀个协程的日志记录时,则自动分配的 id 是非常好的。然而,当⼀个协程与特定请求的处理相关联时或做⼀些特定的后台任务,最好将其明确命名以用于调试目的。
CoroutineName 上下文元素与线程名具有相同的目的。当调试模式开启时,它被包含在正在执行此协程的线程名中。
八、组合上下文中的元素
有时我们需要在协程上下文中定义多个元素。我们可以使用 + 操作符来实现
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
九、协程作用域
kotlinx.coroutines 提供了⼀个封装:CoroutineScope 的抽象。所有的协程构建器都声明为在它之上的扩展。
创建⼀个 CoroutineScope 实例来管理协程的生命周期,并使它与 activity 的生命周期相关联。CoroutineScope 可以通过 CoroutineScope() 创建或者通过MainScope() 工厂函数。前者创建了⼀个通用作用域,而后者为使用 Dispatchers.Main 作为默认调度器的 UI 应用程序创建作用域:
class CoroutineScopeActivity : AppCompatActivity() {
private val mCoroutineScope = MainScope()
private val mHandler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutine_scope)
doSomething()
mHandler.postDelayed({ exit() }, 7000)
}
private fun exit() {
finish()
}
private fun doSomething() {
repeat(10) { i ->
mCoroutineScope.launch {
delay((i + 1) * 2000L)
Log.i("aaaaaaaaaaaa", "Coroutine $i is done")
}
}
}
override fun onDestroy() {
super.onDestroy()
mCoroutineScope.cancel()
}
}
输出:
2022-02-21 19:55:01.654 17770-17770/com.kuaipi.createkotlin I/aaaaaaaaaaaa: Coroutine 0 is done
2022-02-21 19:55:03.645 17770-17770/com.kuaipi.createkotlin I/aaaaaaaaaaaa: Coroutine 1 is done
2022-02-21 19:55:05.643 17770-17770/com.kuaipi.createkotlin I/aaaaaaaaaaaa: Coroutine 2 is done
前三个协程打印了消息,而其它的协程在 Activity.destroy() 调用了 cancel()
十、协程局部数据
ThreadLocal,asContextElement 扩展函数可以将⼀些线程局部数据传递到协程与协程之间。asContextElement 它创建了额外的上下文元素,且保留给定 ThreadLocal 的值,并在每次协程切换其上下文时恢复它。
fun main() = runBlocking {
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()}'")
}
使用 Dispatchers.Default 在后台线程池中启动了⼀个新的协程,所以它工作在线程池中的不同线程中,但它仍然具有线程局部变量的值,我们指定使用 threadLocal.asContextElement(value = "launch") ,无论协程执行在哪个线程中都是没有问题的。
threadLocal有⼀个关键限制,即:当⼀个线程局部变量变化时,则这个新值不会传播给协程调用者(因为上下文元素无法追踪所有 ThreadLocal 对象访问),并且下次挂起时更新的值将丢失。使用 withContext 在协程中更新线程局部变量, 详见asContextElement。
输出
Pre-main, current thread: Thread[main,5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1,5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-1,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main,5,main], thread local value: 'main'