我們在協程的第一篇就已經提過,協程的運行是依賴於線程的。那麼協程與線程之間的關係到底是怎樣的呢?
-
協程上下文(Coroutine Context):多種元素的集合,包括Job、分發器等。協程總是會在某個上下文中執行的,這個上下文是由CoroutineContext類型的一個實例來決定的。
-
協程分發器(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參數,我們目前還沒有介紹過,我們可以通過這個參數,來指定使用哪個協程分發器,從而決定協程運行在哪個線程或者線程池上。
-
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是同一個線程池。
-
如果我們不顯式指定,也就是不帶參數的啓動,那麼它一定會運行在主線程嗎?我們給出的答案是:否。我們不妨在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線程上。
-
我們還剩下一個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掛起函數);當它從掛起函數中恢復執行後,它所運行的線程就變成了掛起函數所在的線程。
-
如果我們想要指定自己創建的線程池來運行協程,那麼我們該怎麼做?
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()這行代碼,然後運行程序,你會發現,雖然控制檯有結果輸出,但是我們的程序並沒有退出,就是由於我們自己創建的線程池一直在佔用着資源。