协程上下文与调度器

368 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情

协程上下文与调度器

协程总是运行在一些以CoroutineContext类型为代表的上下文中,它们被定义在了kotlin的标准库里。

协程上下文是各种不同元素的集合。其中主元素是协程中的job

调度器与线程

协程上下文包含了一个协程调度器,它确定了相关的协程在哪个线程或者哪些线程执行。协程调度器可以将协程限制在一个特定的线程运行,或者将它分派到一个线程池,再或者是让它不受限制地运行。

所有的协程构建器诸如launchasync接收一个可选的CoroutineContext参数,它可以被用来显式地为一个协程或其它上下文元素指定一个调度器,如下面的代码所示:

    /**
     * 下面的代码通过在协程中指定不同的调度器将会在不同的线程中执行
     */
    private fun coroutineTest1() = runBlocking {
        //默认运行在父协程的上下文中,也就是runBlocking主协程
        launch {
            LogUtils.e("main runBlocking: I'm working thread:${getCurrentThreadName()}")
        }
        //下面的协程不受限制 -- 现在是运行在主线程中
        launch (context = Dispatchers.Unconfined){
            LogUtils.e("unconfined: I'm working thread: ${getCurrentThreadName()}")
        }
        //下面的协程使用的是默认的调度器
        launch (context = Dispatchers.Default){
            LogUtils.e("default: I'm working thread:${getCurrentThreadName()}")
        }
        //下面的协程将在新的线程中执行
        launch (context = newSingleThreadContext("MyOwnThread")){
            LogUtils.e("newSingleThreadContext: I'm working thread ${getCurrentThreadName()}")
        }
    }

运行上面的程序,将会得到如下的信息:

14:18:25:737    TAG_ZYF:	unconfined: I'm working thread: main
14:18:25:759    TAG_ZYF:	default: I'm working thread:DefaultDispatcher-worker-1
14:18:25:765    TAG_ZYF:	newSingleThreadContext: I'm working thread MyOwnThread
14:18:25:765    TAG_ZYF:	main runBlocking: I'm working thread:main

按照上面的代码顺序进行分析:

  1. 当使用launch{...}而不传递任何参数的时候,它从启动了它的CoroutineScope中承袭了上下文(以及调度器)。在这个案例中,它从main线程中的runBlocking主协程承袭了上下文。
  2. 当我们指定launch(context = Dispatchers.Unconfined){...}Dispatchers.Unconfined时,这是一个特殊的调度器,且目前来看似乎也运行在main线程中,但实际上它是一种不同的机制。
  3. 当我们指定launch(context = Dispatchers.Default){...}Dispatchers.Default时,这是默认的调度器,它使用共享的后台线程,因此launcher(Dispatchers.Default)GlobalScope.launch{...}使用相同的调度器。
  4. 当我们指定launcher(context = newSingleThreadContext("MyOwnThread"))newSingleThreadContext时,这表示为当前协程的执行指定了一个单独的线程,一个专用的线程是一种非常昂贵的资源,在真实的应用程序中两者都必须被释放,当不再需要的时候,使用close()函数,或存储在一个顶层变量中使它在整个应用程序中被重用。

非受限调度器和受限调度器

Dispatchers.Unconfined协程调度器在调用它的线程启动了一个协程,但它仅仅只是运行到第一个挂起点。挂起后,它恢复线程中的协程,而这完全由被调用的挂起函数来决定。非受限的调度器非常适合执行不消耗CPU时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。

另一方面,该调度器默认继承了外部的CoroutineScope。也就是runBlocking协程的默认调度器,特别是,当它被限制在了调用者线程时,继承自它将会有效地限制协程在该线程运行并且具有可预测地FIFO调度。

    private fun coroutineTest3() = runBlocking {
        launch(Dispatchers.Unconfined) {
            LogUtils.e("Unconfined: before current thread is :${getCurrentThreadName()}")
            doSomething1(1)
            LogUtils.e("Unconfined: after current thread is: ${getCurrentThreadName()}")
        }
        launch {
            LogUtils.e("launch: before current thread is ${getCurrentThreadName()}")
            doSomething1(2)
            LogUtils.e("launch: after current thread is ${getCurrentThreadName()}")
        }
    }


    private suspend fun doSomething1(from: Int){
        delay(1000)
        LogUtils.e("doSomething1:$from current thread:${getCurrentThreadName()}")
    }

运行上面的程序,将会得到下面的输出:

14:56:50:422    TAG_ZYF:	Unconfined: before current thread is :main
14:56:50:434    TAG_ZYF:	launch: before current thread is main
14:56:51:432    TAG_ZYF:	doSomething1:1 current thread:kotlinx.coroutines.DefaultExecutor
14:56:51:432    TAG_ZYF:	Unconfined: after current thread is: kotlinx.coroutines.DefaultExecutor
14:56:51:446    TAG_ZYF:	doSomething1:2 current thread:main
14:56:51:446    TAG_ZYF:	launch: after current thread is main

可以看到,当我们指定协程的调度器为Dispatchers.Unconfined时,一开始协程时运行在main线程中,当遇到第一个挂起点,也就是doSomething1中的delay的时候,协程切换到了默认的执行线程中去执行了。

非受限的调度器是一种高级机制,可以在某些极端情况下提供帮助而不需要调度协程以便稍后执行或产生不希望的副作用,因为某些操作必须立即在协程中执行。非受限的调度器不应该在通常的代码中使用。

上下文中的作业

协程的Job是上下文的一部分,并且可以使用coroutineContext[Job]表达式在上下文中检索它,如下所示:

    private fun coroutineTest4() = runBlocking {
        LogUtils.e("This job is:${coroutineContext[Job]}")
    }

运行上面的代码将会得到如下的输出:

15:24:55:397    TAG_ZYF:	This job is:BlockingCoroutine{Active}@2077d4de

在之前,我们学习过,可以通过isActive来判断当前任务是否被取消,其实这个扩展属性就是coroutineContext[Job]?.isActive == true的一种方便的快捷方式。

子协程

当一个协程被其它协程在CoroutineScope中启动的时候,它将通过CoroutineScope.coroutineContext来承袭上下文,并且这个新协程的Job将会成为父协程作业的子作业。当一个父协程被取消的时候,所有它的子协程也会被递归取消。

然而,当使用GlobalScope来启动一个新协程时,则新协程的作业没有父作业。因此它与这个启动的作用域无关且独立运作。

    private fun coroutineTest5() = runBlocking {
        //启动另一个协程
        val request = launch {
            GlobalScope.launch {
                LogUtils.e("job1: I run in GlobalScope and execute independently")
                delay(1000)
                LogUtils.e("job1: i am not affected by cancellation of the request")
            }

            //子协程
            launch {
                delay(100)
                LogUtils.e("job2: I am a child of request coroutine")
                delay(1000)
                LogUtils.e("job2: I am end")
            }
        }
        delay(500)
        request.cancel()
        delay(1000)
        LogUtils.e("coroutineTest5 end ...")
    }

运行上面的程序将会得到如下的输出信息:

15:53:34:537    TAG_ZYF:	job1: I run in GlobalScope and execute independently
15:53:34:649    TAG_ZYF:	job2: I am a child of request coroutine
15:53:35:539    TAG_ZYF:	job1: i am not affected by cancellation of the request
15:53:36:032    TAG_ZYF:	coroutineTest5 end ...

可以看到,当我们将父协程取消以后,子协程也随之被取消(没有相关子协程的打印信息),但是通过GlobalScope.launch启动的协程不会受到影响,仍然在取消任务之后成功执行完毕。

父协程的职责

一个父协程总是等待所有子协程执行结束。父协程并不会显式地跟踪子协程的启动,并且不必使用Job.join在最后的时候等待它们。

    private fun coroutineTest6() = runBlocking {
        //启动一个协程
        val request = launch {
            repeat(3) {
                launch {
                    delay((it + 1) * 200L)
                    LogUtils.e("child coroutine $it end...")
                }
            }
            LogUtils.e("request coroutine end ... ")
        }
        //等待所有的子协程执行完毕
        request.join()
        LogUtils.e("coroutineTest 6 end ... ")
    }

运行上面的程序,将会得到如下的输出信息:

16:23:48:131    TAG_ZYF:	request coroutine end ... 
16:23:48:336    TAG_ZYF:	child coroutine 0 end...
16:23:48:547    TAG_ZYF:	child coroutine 1 end...
16:23:48:743    TAG_ZYF:	child coroutine 2 end...
16:23:48:743    TAG_ZYF:	coroutineTest 6 end ... 

可以看到,即便request对应的协程中首先输出了request coroutine end ... ,但是仍然会等到所有的子协程执行完毕。

命名协程用于调试

下面的例子演示了对协程进行命名来方便我们进行调试:

    private fun coroutineTest7() = runBlocking(CoroutineName("mainCoroutine")) {
        val one = async(CoroutineName("one")) {
            delay(500)
            LogUtils.e("one running")
            20
        }
        val two = async(CoroutineName("two")) {
            delay(1000)
            LogUtils.e("two running")
            30
        }
        LogUtils.e("coroutineTest7 sum is: ${one.await() + two.await()}")
    }

上面的程序运行结果如下:

16:58:06:392    [main @one#2]	TAG_ZYF:	one running
16:58:06:889    [main @two#3]	TAG_ZYF:	two running
16:58:06:889    [main @mainCoroutine#1]	TAG_ZYF:	coroutineTest7 sum is: 50

可以看到,在输出线程相关的信息的时候带上了协程的信息。要实现这个操作,还需要配置输出的信息携带协程信息,具体配置信息参考文档配置调试信息

结合上下文中的元素

有时我们需要在协程上下文中定义多个元素,我们可以使用+操作符来实现,下面的代码在指定了一个调度器的同时指定了协程的名称:

    /**
     * 结合上下文中的元素,同时给协程添加多个属性,下面例子同时指定了协程的调度器和名称
     */
    private fun coroutineTest8() = runBlocking {
        launch (Dispatchers.Default + CoroutineName("test")){
            delay(1000)
            LogUtils.e("launch end...")
        }
        LogUtils.e("coroutineTest8 end ... ")
    }

上面的代码运行结果如下:

17:11:38:511    [main @coroutine#1]	TAG_ZYF:	coroutineTest8 end ... 
17:11:39:521    [DefaultDispatcher-worker-1 @test#2]	TAG_ZYF:	launch end...

协程作用域

对于Android开发来说,我们经常遇到如下的例子:我们需要在Activity中启动一组协程来执行拉取数据,更新数据,执行动画等相关的操作,所有这些协程必须在Activity退出的时候取消从而避免内存泄露。虽然我们可以手动操作上下文与作业,以结合Activity的生命周期,但是kotlinx.coroutines提供了一个封装:CoroutineScope的抽象。

我们可以通过创建一个CoroutineScope实例来管理协程的生命周期,并使它与Activity的生命周期相关联。CoroutineScope可以通过CoroutineScope()或者MainScope()工厂函数创建。前者创建了一个通用作用域,后者为使用Dispatchers.Main作为默认调度器的UI应用程序创建作用域,如下所示:

  1. 创建Activity演示代码
/**
 * 模拟生成一个Android应用的Activity
 */
class CoroutineTestActivity {

    //创建一个CoroutineScope来管理这个页面所有的协程,如果是在Android应用程序中,可以使用MainScope()创建,
    //但是这里不是Android应用,运行的时候无法找到主线程Looper,会报异常
    private val mCoroutineScope by lazy {
        CoroutineScope(Dispatchers.Default)
    }

    /**
     * 模拟Activity中的onCreate()方法,一般在此处发起数据请求等操作
     */
    fun onCreate(){
        //模拟请求数据的操作
        request()

    }

    /**
     * 模拟Activity中的onDestroy()方法,一般在此处释放资源
     */
    fun onDestroy(){
        //页面结束的时候,取消所有的作业
        mCoroutineScope.cancel()
        //打印模拟页面退出
        LogUtils.e("CoroutineTestActivity destroy ... ")
    }

    /**
     * 模拟请求数据
     */
    private fun request(){
        repeat(10){
            mCoroutineScope.launch(CoroutineName("test-$it")) {
                val time = (it + 1) * 200L
                delay(time)
                //LogUtils.e("time is :$time")
                LogUtils.e("request:$it done ...")
            }
        }
    }
}
  1. 模拟页面的操作
    /**
     * 协程的作用域,模拟Android中的Activity生命周期的相关操作
     */
    private fun coroutineTest9() = runBlocking {
        //打开Activity
        val activity = CoroutineTestActivity()
        activity.onCreate()
        LogUtils.e("CoroutineTestActivity Launch")

        //等待1秒
        delay(1 * 1000)
        //模拟关闭页面
        activity.onDestroy()

        //确认没有任何数据输出
        delay(1000)
    }
  1. 运行代码,得到如下的输出信息:
19:13:51:037    [main]	TAG_ZYF:	programing start
19:13:51:141    [main @coroutine#1]	TAG_ZYF:	CoroutineTestActivity Launch
19:13:51:347    [DefaultDispatcher-worker-1 @test-0#2]	TAG_ZYF:	request:0 done ...
19:13:51:557    [DefaultDispatcher-worker-1 @test-1#3]	TAG_ZYF:	request:1 done ...
19:13:51:750    [DefaultDispatcher-worker-1 @test-2#4]	TAG_ZYF:	request:2 done ...
19:13:51:945    [DefaultDispatcher-worker-1 @test-3#5]	TAG_ZYF:	request:3 done ...
19:13:52:154    [DefaultDispatcher-worker-1 @test-4#6]	TAG_ZYF:	request:4 done ...
19:13:52:158    [main @coroutine#1]	TAG_ZYF:	CoroutineTestActivity destroy ... 
19:13:53:166    [main]	TAG_ZYF:	programing end

从上面的日志信息可以看到,页面退出之后,没有执行完成的协程也没有数据打印出来。