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.png

2.用日志调式

让线程在每⼀个日志文件的日志声明中打印线程的名字,用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'
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容