Kotlin学习系列之:协程上下文与分发器

我们在协程的第一篇就已经提过,协程的运行是依赖于线程的。那么协程与线程之间的关系到底是怎样的呢?

  1. 协程上下文(Coroutine Context):多种元素的集合,包括Job、分发器等。协程总是会在某个上下文中执行的,这个上下文是由CoroutineContext类型的一个实例来决定的。

  2. 协程分发器(Dispatcher):决定协程运行在哪个线程或者线程池上,对应的类就是CoroutineDispatcher。

    再看launch{}和aysnc{}:

    public fun CoroutineScope.launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job
    
    public fun <T> CoroutineScope.async(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> T
    ): Deferred<T>
    

    会存在一个context参数,我们目前还没有介绍过,我们可以通过这个参数,来指定使用哪个协程分发器,从而决定协程运行在哪个线程或者线程池上。

  3. CoroutineDispatcher类。

    /**
     * Base class that shall be extended by all coroutine dispatcher implementations.
     *
     * The following standard implementations are provided by `kotlinx.coroutines` as properties on
     * [Dispatchers] objects:
     *
     * * [Dispatchers.Default] -- is used by all standard builder if no dispatcher nor any other [ContinuationInterceptor]
     *   is specified in their context. It uses a common pool of shared background threads.
     *   This is an appropriate choice for compute-intensive coroutines that consume CPU resources.
     * * [Dispatchers.IO] -- uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive _blocking_
     *   operations (like file I/O and blocking socket I/O).
     * * [Dispatchers.Unconfined] -- starts coroutine execution in the current call-frame until the first suspension.
     *   On first  suspension the coroutine builder function returns.
     *   The coroutine resumes in whatever thread that is used by the
     *   corresponding suspending function, without confining it to any specific thread or pool.
     *   **Unconfined dispatcher should not be normally used in code**.
     * * Private thread pools can be created with [newSingleThreadContext] and [newFixedThreadPoolContext].
     * * An arbitrary [Executor][java.util.concurrent.Executor] can be converted to dispatcher with [asCoroutineDispatcher] extension function.
     *
     * This class ensures that debugging facilities in [newCoroutineContext] function work properly.
     */
    

    这是CoroutineDispatcher类的文档说明:CoroutineDispatcher类是所有协程分发器实现的基类。它有如下标准实现(作为Dispatchers类的属性存在):

    • Dispatchers.Default:被用于所有没有指定分发器或者ContinuationInterceptor的标准协程构建器,简而言之,就是默认的分发器。它会使用一个共享的后台线程池。
    • Dispatchers.IO:也会使用一个共享线程池,主要用于IO操作。
    • Dispatchers.MAIN:主线程分发器,即启动的协程会在主线程中运行
    • Dispatchers.Unconfined:不指定某个特定的线程或者线程池来运行协程
    • 自定义的私有线程池

    下面我们就通过一个例子来说明这些协程分发器的作用:

    fun main() = runBlocking<Unit> {
    
        launch {
            println("No params, thread : ${Thread.currentThread().name}")
        }
    
        launch(Dispatchers.Unconfined) {
            println("Unconfined, thread: ${Thread.currentThread().name}")
        }
    
        launch(Dispatchers.Default) {
            println("Default, thread: ${Thread.currentThread().name}")
        }
    
        GlobalScope.launch {
            println("GlobalScope, thread: ${Thread.currentThread().name}")
        }
    }
    

    先看运行结果:

    Unconfined, thread: main
    Default, thread: DefaultDispatcher-worker-1
    GlobalScope, thread: DefaultDispatcher-worker-3
    No params, thread : main

    可能每个人的输出结果的顺序都不太一样,这是正常的,但是每行的结果是一样的。

    • 不指定参数:main线程

    • 使用Dispathcers.Unconfined:此时显示的是在main线程,但是它的运行机制没有这么简单,我们在下面详述

    • 使用Dispathcers.Default:DefaultDispatcher-worker-1,很明显,就不在主线程了,这个就是我们所说的后台的共享线程池

    • GlobalScope.launch{}:会发现它也不在主线程,并且和Dispathcers.Default是同一个线程池。

  4. 如果我们不显式指定,也就是不带参数的启动,那么它一定会运行在主线程吗?我们给出的答案是:否。我们不妨在launch(Dispatchers.Default){}中再添加几行代码:

    launch(Dispatchers.Default) {
        println("Default, thread: ${Thread.currentThread().name}")
        launch {
            println("In Default, no params, thread: ${Thread.currentThread().name}")
        }
    }
    

    现在的运行结果为:

    Unconfined, thread: main
    Default, thread: DefaultDispatcher-worker-1
    In Default, no params, thread: DefaultDispatcher-worker-3
    GlobalScope, thread: DefaultDispatcher-worker-2
    No params, thread : main

    注意第三行和第五行,同样的不带参数的launch,它所运行的线程是不一样的。

    好,我们现在来总结协程的运行线程的判定:如果我们没有显式指定分发器,那么它会考虑从启动它的协程上下文去继承;如果我们显式指定了分发器,那么就使用指定的分发器来运行线程。如这里在Default中由launch{}启动的协程,它就会从外层launch(Dispatchers.Default)继承过来,再考虑外层的launch{}(输出结果为No params, thread : main),它是由runBlocking{}继承过来,由于runBlocking是运行在主线程中,所以它也是运行在主线程中。如果你还疑问runBlocking为什么运行在主线程,我们来看看runBlocking的实现:

    public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
        val currentThread = Thread.currentThread()
        val contextInterceptor = context[ContinuationInterceptor]
        val eventLoop: EventLoop?
        val newContext: CoroutineContext
        if (contextInterceptor == null) {
            // create or use private event loop if no dispatcher is specified
            eventLoop = ThreadLocalEventLoop.eventLoop
            newContext = GlobalScope.newCoroutineContext(context + eventLoop)
        } else {
            // See if context's interceptor is an event loop that we shall use (to support TestContext)
            // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
            eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
                ?: ThreadLocalEventLoop.currentOrNull()
            newContext = GlobalScope.newCoroutineContext(context)
        }
        val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
        coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
        return coroutine.joinBlocking()
    }
    

    可以看到,它会使用 Thread.currentThread()获取当前它所在的线程,由于我们这里阻塞的是main线程,所以它自然会运行在main线程上。

  5. 我们还剩下一个Dispatchers.Unconfined没有讲解:

    fun main() = runBlocking<Unit> {
    
        launch(Dispatchers.Unconfined) {
            println("before delay: thread -> " + Thread.currentThread().name)
            delay(100)
            println("after delay: thread -> " + Thread.currentThread().name)
        }
    }
    

    输出结果为:

    before delay: thread -> main
    after delay: thread -> kotlinx.coroutines.DefaultExecutor

    会发现在调用delay方法前后,它所运行的线程是不一样的。那么它的运行机制到底是怎样的呢?使用Dispatchers.Unconfined分发器的协程,它会在运行在启动它的协程上下文中去继承,这里也就是main线程,直到遇到第一个挂起点(也就是这里的delay挂起函数);当它从挂起函数中恢复执行后,它所运行的线程就变成了挂起函数所在的线程。

  6. 如果我们想要指定自己创建的线程池来运行协程,那么我们该怎么做?

    fun main() = runBlocking<Unit> {
    
        val executorDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
        launch(executorDispatcher) {
            println("Executors, thread: ${Thread.currentThread().name}")
            executorDispatcher.close()
        }
    }
    

    Executors, thread: pool-1-thread-1

    通过asCoroutineDispatcher()这个扩展方法,我们可以将newSingleThreadExecutor的线程池转换成一个分发器,然后使用这个分发器去启动我们的协程。这里有一个注意点,那就是一定要调用close关闭这个分发器。大家可以尝试注释掉executorDispatcher.close()这行代码,然后运行程序,你会发现,虽然控制台有结果输出,但是我们的程序并没有退出,就是由于我们自己创建的线程池一直在占用着资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值