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()這行代碼,然後運行程序,你會發現,雖然控制檯有結果輸出,但是我們的程序並沒有退出,就是由於我們自己創建的線程池一直在佔用着資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章