一、前言
? 协程总是在由CoroutineContext类型表示的某个上下文中执行。它是由一组元素构成的。主要是Job 和Dispatchers 构成的
二、Dispatchers
? Dispatchers来确定协程由哪个或者哪些线程来执行。也可以使协程不受限制的运行。以下是参考例子
@Test
fun dispather(){
runBlocking {
launch {
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继承上下文(以及调度程序)。在这种情况下,它继承了runBlocking 在main 线程中运行的主协程的上下文。
Dispatchers.Unconfined是一个特殊的调度器,它看起来也运行在main 线程中,但实际上它是一种不同的机制,稍后解释。
在范围中没有明确指定其他调度程序时使用的默认调度程序。它由Dispatchers.Default表示,并使用共享的后台线程池。
newSingleThreadContext创建一个线程供协程运行。专用线程是一种非常昂贵的资源。在实际应用程序中,它必须在不再需要时使用close函数释放,或者存储在顶级变量中并在整个应用程序中重用。
三、Dispatchers.Unconfined
这里看下不受限和受限的协程运行情况
@Test
fun unconfined(){
runBlocking {
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 {
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
可以看出来受限的一直是同一个线程挂起和恢复,而不受限的则是由main线程挂起,默认线程池恢复。所以不受限的协程不由一个线程控制。但是具体使用场景暂时不太清楚。官方推荐的场景如下:
unconfined dispatcher 是一种高级机制,在某些特殊情况下很有帮助,在这种情况下,不需要为稍后执行的协程分派或产生不良副作用,因为协程中的某些操作必须立即执行。unconfined dispatcher 不应该用在通用代码中。
四、线程之间的跳转
协程可以在不同线程之间进行切换
@OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
@Test
fun switchThread(){
runBlocking {
newSingleThreadContext("Ctx1").use { ctx1 ->
newSingleThreadContext("Ctx2").use { ctx2 ->
runBlocking(ctx1) {
println("Started in ctx1")
withContext(ctx2) {
println("Working in ctx2")
}
println("Back to ctx1")
}
}
}
}
}
需要注意的是该方式在将来可能会被替换掉,当使用线程时候需要注意线程释放的问题,线程池的释放可以使用ExecutorCoroutineDispatcher.close() 函数
五、协程及其子协程
一般来说在协程之上创建的协程受该协程管理,比如在A协程上创建B协程,那么A协程取消后,B协程也会取消。可以使用以下两种方式对其进行修改
-
启动协程时候指定不同的范围,如下所示 @Test
fun childJob(){
runBlocking {
val request = launch {
launch(Job()) {
println("job1: I run in my own Job 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 my own Job 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?
当延迟500ms后取消协程,Job1依然输出了延迟后的内容,Job2却没有。所以可以看出Job1不受父协程控制 -
如果在启动协程时显式指定了不同的范围(例如,GlobalScope.launch ,或者自己定义的协程范围在另外一个协程中启动),则它不会Job 从父范围继承 。
六、家长责任
一般来说,父协程取消后,子协程就会跟着取消。但是一般来说,如果父协程不主动取消的话,会等子协程全部执行完毕再取消。除非,我们要等到协程执行完获取数据再进行下一步操作。否则不要使用Job.join()
七、协程的名字
出于调试的目的,可能会需要对协程进行命名,可以使用CoroutineName 来进行处理
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
@Test
fun coroutineName(){
runBlocking {
println("Started main coroutine")
val v1 = async(CoroutineName("v1coroutine")) {
delay(500)
println("Computing v1")
252
}
val v2 = async(CoroutineName("v2coroutine")) {
delay(1000)
println("Computing v2")
6
}
println("The answer for v1 / v2 = ${v1.await() / v2.await()}")
}
}
运行结果如下:
[Test worker @coroutine#1] Started main coroutine
[Test worker @v1coroutine#2] Computing v1
[Test worker @v2coroutine#3] Computing v2
[Test worker @coroutine#1] The answer for v1 / v2 = 42
八、元素组合
之前提过CoroutineContext可以由多个元素构成的,所以我们在实际传入该元素时候可以由多个元素进行组合,组合方式使用+ 。如下
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
结果如下
I'm working in thread DefaultDispatcher-worker-1 @test#2
九、协程范围
没有协程都有自己的范围,一般在定义时候就会自动生成范围CoroutineScope 。有时候我们需要主动去控制协程范围,比如和页面生命周期绑定。在页面销毁的时候也把协程销毁掉。CoroutineScope 实例可以由创建CoroutineScope()或MainScope()工厂函数。前者创建一个通用范围,而后者创建一个 UI 应用程序范围并使用Dispatchers.Main作为默认调度程序:
这里我们使用MainScope 来进行演示
class Activity {
private val mainScope = MainScope()
fun destroy() {
mainScope.cancel()
}
fun doSomething() {
repeat(10) { i ->
mainScope.launch {
delay((i + 1) * 200L)
println("Coroutine $i is done")
}
}
}
}
}
使用情况如下:
val activity = Activity()
activity.doSomething()
println("Launched coroutines")
delay(500L)
println("Destroying activity!")
activity.destroy()
delay(1000)
不过实际上Android对这个有专门的支持。不需要这么麻烦
十、读取本地数据
本段实际上并不常用。不过作为学习记录一下
具体参考以下会更好一点
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-context-element.html
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()}'")
输出结果如下
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方法并在不正确的使用中快速失败。
十一、参考文档
-
协程的Dispatchers和CoroutineContext https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html -
Kotlin Cheatsheet: Coroutine Dispatchers https://medium.com/dont-code-me-on-that/kotlin-cheatsheet-coroutine-dispatchers-1dc80f08bdd6 -
asContextElement https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-context-element.html
|