协程上下文和调度器
协程上下文和调度器.jpg
调度器和线程 以及Unconfined调度器作用
- 定义:确定执行协程的目标载体 即运行在哪个线程 包含一个还是多个线程
作用:
(1). 可以将协程的操作限定在特定的线程上
(2). 可以将协程分配到线程池
(3). 可以将协程无限的运行
所有的协程构造器(比如 launch 、async)都接收一个可选参数 即CoroutineContext 该参数可以显示的指定要创建的协程和其它上下文元素所要使用的调度器
- 例子:
/**
* 调度器和线程
* 理解Unconfined
*/
fun main() = runBlocking<Unit> {
// 不带参数 默认继承外部runBlocking主协程的上下文和调度器
val job1 = launch { println("runBlocking ${Thread.currentThread().name}") }
// Dispatchers.Default 特殊的调度器 看起来似乎也是在主线程中运行 但是实则是一种不同的机制 稍后会分析
val job2 = launch(Dispatchers.Default) { println("Default ${Thread.currentThread().name}") }
// Dispatchers.Unconfined 调度器 GlobalScope中启动的协程默认使用Dispatchers.Unconfined调度器 并使用共享的线程池
// 因此GlobalScope.launch{}和launch(Dispatchers.Default)使用的是相同的调度器
// Dispatchers.Unconfined是一种高级机制 可以在某些极端的情况下 提供帮助不需要协程调度以便于稍后执行 也就是说协程挂起后 某些操作需要
// 立即在协程中执行 非受限调度器不应该在一般的代码中调用
val job3 = launch(Dispatchers.Unconfined) { println("Unconfined ${Thread.currentThread().name}") }
// newSingleThreadContext("名称")用于为协程专门创建一个新的线程来运行 专用线程是非常昂贵的资源
// 实际应用中 不使用了 需要close()释放掉 或者存储在顶级变量中以此来实现在整个应用中重用
val job4 = launch(newSingleThreadContext("MyOwnThread")) { println("newSingleThreadContext ${Thread.currentThread().name}") }
// 上述代码执行结果
/*Unconfined main
Default DefaultDispatcher-worker-2
newSingleThreadContext MyOwnThread
runBlocking main*/
// 对比一下 Unconfined 和 confined Dispatcher(调度器)
// 例子
val job5 = launch {
println("默认runBlocking调度器 挂起前运行线程:${Thread.currentThread().name} ")
delay(1000L)
println("默认runBlocking调度器 挂起后运行线程:${Thread.currentThread().name} ")
}
val job6 = launch(Dispatchers.Unconfined) {
println("Unconfined调度器 挂起前运行线程:${Thread.currentThread().name} ")
delay(1000L)
println("Unconfined调度器 挂起后运行线程:${Thread.currentThread().name} ")
}
}
注意理解:Unconfined调度器在调用者线程中启动一个协程,但它只仅仅是运行到第一个挂起点
在挂起之后,它将恢复线程中的协程 该协程完全由挂起的函数决定
Unconfined调度器适用于既不消耗cpu时间和不更新任何受限制于特定的线程的共享数据(如UI)的协程
调试协程和线程
协程可以在一个线程上挂起,可以在另一个协程中继续运行,即使使用单线程的调度器,那也很难知道当前协程在做什么、在哪里、什么状态,所以说调试线程常用的方法就是在线程中打印的所有日志语句上添加线程名,日志框架普遍支持,当使用协程时,线程名本身没有提供太多的上下文信息,因此 kotlinx.coroutines 包含了调试工具以便使协程调试起来更加容易
- 开启 JVM 的 -Dkotlinx.coroutines.debug 配置后运行以下代码
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()}")
}
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
执行结果:
[main] I'm computing a piece of the answer
[main] I'm computing another piece of the answer
[main] The answer is 42
在线程间切换
注意:使用了 kotlin 标准库中的 use 函数用来在不再需要时释放 newSingleThreadContext 所创建的线程
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")
}
}
}
}
执行结果:
[Ctx1] Started in ctx1
[Ctx2] Working in ctx2
[Ctx1] Back to ctx1
子协程
当一个协程在另一个协程的协程作用域中启动时,它将通过CoroutineScope.coroutineContext继承上下文
新启动的协程的Job是父协程Job的子Job 当父协程被取消时,其所有的子协程也会递归地被取消
当使用GlobalScope启动协程时 该协程没有父Job 也就是说它不受其启动作用域和独立运作范围的限制
例子:
fun main() = runBlocking {
val job = launch {
GlobalScope.launch {
println(" GlobalScope启动的子协程开始执行 ")
delay(1000L)
// 按照现有逻辑 子协程在父协程取消后 也应该被递归取消 下面的打印不会输出才对
// 然而并不是上面描述的那样 因为使用 GlobalScope 启动的协程 Job 是没有父级的,
// 因此,它不受其启动的作用域和独立运作范围的限制
println(" GlobalScope启动的子协程执行结束 ")
}
launch {
delay(100L)
println(" 子协程开始执行 ")
delay(1000L)
println(" 子协程执行结束 ")
}
}
delay(500L)
println(" 父协程取消前 ")
job.cancelAndJoin()
println(" 父协程取消后 ")
delay(1000L)
println(" 主线程执行完毕 主协程被取消 ")
}
执行结果:
GlobalScope启动的子协程开始执行
子协程开始执行
父协程取消前
父协程取消后
GlobalScope启动的子协程执行结束
主线程执行完毕 主协程被取消
父协程的职责
- 父协程总会等待其所有子协程执行完成
- 父协程不必显式跟踪它启动的所有子协程 也不必在末尾Job.join()等待子协程完成
fun main() = runBlocking {
val request = launch {
repeat(1000) {
// 子协程
launch {
delay((it + 1) + 200L)
println("子协程 $it 执行完成")
}
}
println("父协程执行完毕")
}
request.join()// 阻塞等待所有子协程执行完毕
println("主协程执行结束")
}
执行结果:
父协程执行完毕
子协程 0 执行完成
子协程 1 执行完成
子协程 2 执行完成
主协程执行结束
为协程命名以便调试
- 当一个协程绑定到一个特定请求的处理或者执行某个特定的后台任务时,最好显式地为它命名,以便进行调试
fun main() = runBlocking(CoroutineName("main")) {
logI("Started main coroutine")
val v1 = async(CoroutineName("v1coroutine")) {
delay(500)
logI("Computing v1")
252
}
val v2 = async(CoroutineName("v2coroutine")) {
delay(1000)
logI("Computing v2")
6
}
}
fun logI(msg: String) = println("[${Thread.currentThread().name}] $msg")
执行结果:
[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2