协程上下⽂与调度器

  协程总是运⾏在⼀些以 CoroutineContext 类型为代表的上下⽂中,它们被定义在了 Kotlin 的标准库 ⾥。 协程上下⽂是各种不同元素的集合。其中主元素是协程中的 Job,我们在前⾯的⽂档中⻅过它以及它的 调度器,⽽本⽂将对它进⾏介绍。

调度器与线程

  协程上下⽂包含⼀个 协程调度器(参⻅ CoroutineDispatcher)它确定了哪些线程或与线程相对应的 协程执⾏。协程调度器可以将协程限制在⼀个特定的线程执⾏,或将它分派到⼀个线程池,亦或是让它 不受限地运⾏。 所有的协程构建器诸如 launch 和 async 接收⼀个可选的 CoroutineContext 参数,它可以被⽤来显式 的为⼀个新协程或其它上下⽂元素指定⼀个调度器。

  尝试下⾯的⽰例:

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
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking : I'm working in thread main

  当调⽤ launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下⽂(以及调度 器)。在这个案例中,它从 main 线程中的 runBlocking 主协程承袭了上下⽂。 Dispatchers.Unconfined 是⼀个特殊的调度器且似乎也运⾏在 main 线程中,但实际上,它是⼀种不 同的机制,这会在后⽂中讲到。 当协程在 GlobalScope 中启动时,使⽤的是由 Dispatchers.Default 代表的默认调度器。默认调度器使 ⽤共享的后台线程池。所以 launch(Dispatchers.Default) { …… } 与 GlobalScope.launch { …… } 使⽤相同的调度器。 newSingleThreadContext 为协程的运⾏启动了⼀个线程。⼀个专⽤的线程是⼀种⾮常昂贵的资源。 在真实的应⽤程序中两者都必须被释放,当不再需要的时候,使⽤ close 函数,或存储在⼀个顶层变量 中使它在整个应⽤程序中被重⽤。

⾮受限调度器 vs 受限调度器

  Dispatchers.Unconfined 协程调度器在调⽤它的线程启动了⼀个协程,但它仅仅只是运⾏到第⼀个挂 起点。挂起后,它恢复线程中的协程,⽽这完全由被调⽤的挂起函数来决定。⾮受限的调度器⾮常适⽤于 执⾏不消耗 CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。 另⼀⽅⾯,该调度器默认继承了外部的 CoroutineScope。runBlocking 协程的默认调度器,特别是,当 它被限制在了调⽤者线程时,继承⾃它将会有效地限制协程在该线程运⾏并且具有可预测的 FIFO 调 度。

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 {...} 协程并在 main 线程中运⾏,当 delay 函数调 ⽤的时候,⾮受限的那个协程在默认的执⾏者线程中恢复执⾏。

⾮受限的调度器是⼀种⾼级机制,可以在某些极端情况下提供帮助⽽不需要调度协程以便稍后执⾏或产⽣不希望的副作⽤,因为某些操作必须⽴即在协程中执⾏。⾮受限调度器不应该在通常的
代码中使⽤。

 

调试协程与线程

  协程可以在⼀个线程上挂起并在其它线程上恢复。甚⾄⼀个单线程的调度器也是难以弄清楚协程在何 时何地正在做什么事情。使⽤通常调试应⽤程序的⽅法是让线程在每⼀个⽇志⽂件的⽇志声明中打印 线程的名字。这种特性在⽇志框架中是普遍受⽀持的。但是在使⽤协程时,单独的线程名称不会给出很 多协程上下⽂信息,所以 kotlinx.coroutines 包含了调试⼯具来让它更简单。 使⽤ -Dkotlinx.coroutines.debug JVM 参数运⾏下⾯的代码:

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 内的主协程 (#1),以及计算延期的值的另外两个协程 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 函数在⽅括号种打印了线程的名字,并且你可以看到它是 main 线程,并且附带了当前正在 其上执⾏的协程的标识符。这个标识符在调试模式开启时,将连续分配给所有创建的协程。

  当 JVM 以 -ea 参数配置运⾏时,调试模式也会开启。你可以在 DEBUG_PROPERTY_NAME 属性 的⽂档中阅读有关调试⼯具的更多信息。

在不同线程间跳转

  使⽤ -Dkotlinx.coroutines.debug JVM 参数运⾏下⾯的代码

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

  注意,在这个例⼦中,当我们不再需要某个在 newSingleThreadContext 中创建的线程的时候,它使⽤ 了 Kotlin 标准库中的 use 函数来释放该线程。

上下⽂中的作业

  协程的 Job 是上下⽂的⼀部分,并且可以使⽤ coroutineContext [Job] 表达式在上下⽂中检索 它:

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 来启动⼀个协程时,则新协程的作业没有⽗作业。因此它与这个启动的作⽤域⽆关且独⽴运作。

  

// 启动⼀个协程来处理某种传⼊请求(request)
val request = launch {
// 孵化了两个⼦作业, 其中⼀个通过 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() // 取消请求(request)的执⾏
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 在最后的时候等待它们:

// 启动⼀个协程来处理某种传⼊请求(request)
val request = launch {
repeat(3) { i -> // 启动少量的⼦作业
launch {
delay((i + 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒的时间
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 上下⽂元素与线程名具有相同的⽬的。当调试模式开启时,它被包含在正在执 ⾏此协程的线程名中。

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

  

结合上下文中的元素

  有时我们需要在协程上下⽂中定义多个元素。我们可以使⽤ + 操作符来实现。⽐如说,我们可以显式指 定⼀个调度器来启动协程并且同时显式指定⼀个命名:

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

  

协程作用域

  让我们将关于上下⽂,⼦协程以及作业的知识综合在⼀起。假设我们的应⽤程序拥有⼀个具有⽣命周期 的对象,但这个对象并不是⼀个协程。举例来说,我们编写了⼀个 Android 应⽤程序并在 Android 的 activity 上下⽂中启动了⼀组协程来使⽤异步操作拉取并更新数据以及执⾏动画等等。所有这些协程必 须在这个 activity 销毁的时候取消以避免内存泄漏。当然,我们也可以⼿动操作上下⽂与作业,以结合 activity 的⽣命周期与它的协程,但是 kotlinx.coroutines 提供了⼀个封装:CoroutineScope 的 抽象。你应该已经熟悉了协程作⽤域,因为所有的协程构建器都声明为在它之上的扩展。 我们通过创建⼀个 CoroutineScope 实例来管理协程的⽣命周期,并使它与 activit 的⽣命周期相关 联。CoroutineScope 可以通过 CoroutineScope() 创建或者通过MainScope() ⼯⼚函数。前者创建 了⼀个通⽤作⽤域,⽽后者为使⽤ Dispatchers.Main 作为默认调度器的 UI 应⽤程序 创建作⽤域:

  

class Activity {
private val mainScope = MainScope()
fun destroy() {
mainScope.cancel()
}
// 继续运⾏……
// 在 Activity 类中
fun doSomething() {
// 在⽰例中启动了 10 个协程,且每个都⼯作了不同的时⻓
repeat(10) { i ->
mainScope.launch {
delay((i + 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒等等不同的时间
println("Coroutine $i is done")
}
}
}
} // Activity 类结束

  在 main 函数中我们创建 activity,调⽤测试函数 doSomething ,并且在 500 毫秒后销毁这个 activity。这取消了从 doSomething 启动的所有协程。我们可以观察到这些是由于在销毁之后,即使 我们再等⼀会⼉,activity 也不再打印消息。

val activity = 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 的值,并在每次协程切换其上下⽂时恢复它。               它很容易在下⾯的代码中演⽰:

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") ,⽆论协程执⾏在什么线程中都是没有问题的。因此,其输出如(调试)所⽰:

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'

  这很容易忘记去设置相应的上下⽂元素。如果运⾏协程的线程不同,在协程中访问的线程局部变量则可 能会产⽣意外的值。为了避免这种情况,建议使⽤ ensurePresent ⽅法并且在不正确的使⽤时快速失 败。 ThreadLocal 具有⼀流的⽀持,可以与任何 kotlinx.coroutines 提供的原语⼀起使⽤。但它有 ⼀个关键限制,即:当⼀个线程局部变量变化时,则这个新值不会传播给协程调⽤者(因为上下⽂元素⽆ 法追踪所有 ThreadLocal 对象访问),并且下次挂起时更新的值将丢失。使⽤ withContext 在协程 中更新线程局部变量,详⻅ asContextElement。 另外,⼀个值可以存储在⼀个可变的域中,例如 class Counter(var i: Int) ,是的,反过来,可以 存储在线程局部的变量中。然⽽,在这个案例中你完全有责任来进⾏同步可能的对这个可变的域进⾏的 并发的修改。 对于⾼级的使⽤,例如,那些在内部使⽤线程局部传递数据的⽤于与⽇志记录 MDC 集成,以及事务上下 ⽂或任何其它库,请参⻅需要实现的 ThreadContextElement 接⼝的⽂档。

 

kotlin协程——>协程上下文与调度器的更多相关文章

  1. go调度: 第二部分-go调度器

    前言 这个博客是三部分中提供go调度器的语义和机制的部分. 博客三部分的顺序: 1) go调度: 第一部分-操作系统调度 2) go调度: 第二部分-go调度器 3) go调度: 第三部分-并发 介绍 ...

  2. Erlang 进程被抢占的条件——一个进程长时霸占调度器的极端示例

    最近研究 binary 的实现和各种操作对应的 beam 虚拟机汇编指令,发现有一些指令序列是不可重入的,比如说有的指令构造一个上下文(也就是某种全局状态),然后下一条指令会对这个上下文做操作(具体的 ...

  3. kotlin学习-Coroutines(协程)

    协程(又名纤程),轻量级线程(建立在线程基础上,属于用户态调用),非阻塞式编程(像同步编写一样),在用户态内进行任务调度,避免与内核态过多交互问题,提高程序快速响应.协程使用挂起当前上下文替代阻塞,被 ...

  4. Python协程中使用上下文

    在Python 3.7中,asyncio 协程加入了对上下文的支持.使用上下文就可以在一些场景下隐式地传递变量,比如数据库连接session等,而不需要在所有方法调用显示地传递这些变量.使用得当的话, ...

  5. golang中goroutine协程调度器设计策略

    goroutine与线程 /* goroutine与线程1. 可增长的栈os线程一般都有固定的栈内存,通常为2MB,一个goroutine的在其声明周期开始时只有很小的栈(2KB),goroutine ...

  6. Python全栈开发-Day10-进程/协程/异步IO/IO多路复用

    本节内容 多进程multiprocessing 进程间的通讯 协程 论事件驱动与异步IO Select\Poll\Epoll——IO多路复用   1.多进程multiprocessing Python ...

  7. JAVA协程 纤程 与Quasar 框架

    ava使用的是系统级线程,也就是说,每次调用new Thread(....).run(),都会在系统层面建立一个新的线程,然鹅新建线程的开销是很大的(每个线程默认情况下会占用1MB的内存空间,当然你愿 ...

  8. Erlang/OTP 17.0-rc1 新引入的"脏调度器"浅析

    最近在做一些和 NIF 有关的事情,看到 OTP 团队发布的 17 rc1 引入了一个新的特性“脏调度器”,为的是解决 NIF 运行时间过长耗死调度器的问题.本文首先简单介绍脏调度器机制的用法,然后简 ...

  9. 重新梳理调度器——GMP 调度模型

    调度器--GMP 调度模型 Goroutine 调度器,它是负责在工作线程上分发准备运行的 goroutines. 首先在讲 GMP 调度模型之前,我们先了解为什么会有这个模型,之前的调度模型是什么样 ...

  10. k8s调度器介绍(调度框架版本)

    从一个pod的创建开始 由kubectl解析创建pod的yaml,发送创建pod请求到APIServer. APIServer首先做权限认证,然后检查信息并把数据存储到ETCD里,创建deployme ...

随机推荐

  1. 【MongoDB】Re05 分片集群(Win平台搭建)

    分片副本集1 (3实例)  主1 从1 裁1 分片副本集2 (3实例)  主1 从1 裁1 配置副本集(3实例) 主1 从2 路由(2配置) 用Windows平台搭建 配置目录设置: ├─config ...

  2. 【OracleDB】 06 多表查询

    多表查询的目的: 从两张以上的数据表中查询每张表的字段 笛卡尔集的问题: 查询职员表一共107条记录 select count(employee_id) from employees; 查询部门表,一 ...

  3. 如何访问SCI-Hub上的资源?

    答案: 使用tor访问.onion网络资源. tor 下载地址: https://www.torproject.org/ 如果不使用tor方式访问可能会无法访问,被提示:

  4. Jax框架的性能分析——性能分析可视化

    官方文档: https://jax.readthedocs.io/en/latest/profiling.html 将jax代码的性能文件写入到文件夹中,并给出上传第三方网站的链接生成(https:/ ...

  5. 【转载】 linux中umask命令介绍

    版权声明:本文为CSDN博主「立二拆四i」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/weixin_4408 ...

  6. 2022 CCF推荐会议列表(国际会议列表)

    2019年版本的CCF推荐会议和期刊: https://www.ccf.org.cn/Academic_Evaluation/By_category/2022-12-02/780280.shtml 2 ...

  7. NoSQL之事务和相关特性

    NoSQL 简介 NoSQL(NoSQL = Not Only SQL ),意即"不仅仅是SQL". 在现代的计算系统上每天网络上都会产生庞大的数据量. 这些数据有很大一部分是由关 ...

  8. 一步步开发Windows服务(Windows Service)[转]

    基于0起步来创建一个服务,做到简单的记录时间日志功能,其具体招行方法可自行添加. 1.创建服务 2.删除默认服务文件 3.添加自己的服务文件 4.更改启动项目 5. 引用 using System.T ...

  9. 11. 基于ARM Cortex-A9中断详解

    一.中断概念 操作系统中,中断是很重要的组成部分.出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行. 有了中断系统才可以不用一直 ...

  10. C#ListView类的继承

    ListView控件类新加方法 新建一个类myListView class myListView : System.Windows.Forms.ListView { //添加自定义的方法 -- //设 ...