與其說協程是一個輕量級線程,我更願意把它當然一個個待執行/可執行的任務。這樣就引申出一個問題——協程是運行在哪個線程上的?這就是本篇文章想要探討的問題,同時我們也將學習如何讓協程在特定的線程裏執行。
首先來看一個例子:
fun log(msg: String) {
println("[${Thread.currentThread().name}] $msg")
}
fun main() = runBlocking {
val job1 = GlobalScope.launch {
log("launch before delay")
delay(100)
log("launch after delay")
}
val job2 = GlobalScope.launch {
log("launch2 before delay")
delay(200)
log("launch2 after delay")
}
job1.join()
job2.join()
}
下面是在我機器上的一個輸出(需要加入 JVM 參數 -Dkotlinx.coroutines.debug
纔會打印協程名):
[DefaultDispatcher-worker-2 @coroutine#3] launch2 before delay
[DefaultDispatcher-worker-1 @coroutine#2] launch before delay
[DefaultDispatcher-worker-1 @coroutine#2] launch after delay
[DefaultDispatcher-worker-1 @coroutine#3] launch2 after delay
這個輸出有兩個要點:
- 從線程名推斷,兩個協程很可能運行在某個線程池中
- 第二個協程先運行在 worker-2,然後又運行在 worker-1
關於第一點, launch
的文檔有這麼一句話:
If the context does not have any dispatcher nor any other
ContinuationInterceptor
, thenDispatchers.Default
is used.
這裏引入了本篇文章的主題——Distpacher,正是它決定了協程運行在哪個線程裏。Dispatcher 的問題我們馬上會談到,我們先看看第二個問題。
第二個協程先運行在 worker-2,然後又運行在 worker-1。這提醒我們,很多時候不能假設協程會運行在同一個線程裏,它唯一保證的是,協程中的代碼會串行執行。由於協程是串行執行的,即使前後不是在同一個線程,我們也能安全地對局部變量進行讀寫:
suspend fun foo() {
val list = someSuspendFn()
val list2 = someOtherSuspendFn()
// cope with list/list2
}
現在我們回到本篇文章的重點——Dispatcher。所謂的 Dispatcher,中文我們可以叫它分發器,是用來將協程分發到特定線程去執行的。它的接口是 ContinuationInterceptor
:
interface ContinuationInterceptor : CoroutineContext.Element {
fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
}
注意,它繼承了 CoroutineContext.Element
,所以他也是一個 context。
Continuation
代表着協程運行的某個中間運行狀態:
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
當需要繼續執行協程時,Kotlin 會調用它的 resumeWith
方法。ContinuationInterceptor
的 interceptContinuation
可以返回一個新的 Continuation
,在這個新的 Continuation
的 resumeWith
裏面,我們可以讓協程運行在任意的線程裏。
下面我們看一個最簡單的 Dispatcher:
class DummyDispatcher
: AbstractCoroutineContextElement(ContinuationInterceptor),
ContinuationInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
return continuation
}
}
fun main() = runBlocking {
val dispatcher = DummyDispatcher()
log("which thread am I in?")
val job = GlobalScope.launch(dispatcher) {
log("launch before delay")
delay(100)
log("launch after delay")
}
job.join()
}
由於我們的 DummyDispatcher
什麼也沒做,協程會繼續在原來的線程中執行。運行結果爲:
[main @coroutine#1] which thread am I in?
[main @coroutine#1] launch before delay
[kotlinx.coroutines.DefaultExecutor] launch after delay
一開始 runBlocking
所在的線程爲 main,由於我們返回了原始的 continuation
,所以在 delay
前的那個調用還是在線程 main;最後一個打印之所以換到了另一個線程,是因爲 delay
內部使用了這個 DefaultExecutor 來實現延遲執行。
現在我們來構造一個真正的 ContinuationInterceptor
,它會讓協程運行在我們創建的線程池裏:
class SingleThreadedDispatcher
: AbstractCoroutineContextElement(ContinuationInterceptor),
ContinuationInterceptor {
private val executorService = Executors.newSingleThreadExecutor {
Thread(it, "SingleThreadedDispatcher")
}
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
return OurContinuation(continuation)
}
inner class OurContinuation<T>(private val origin: Continuation<T>) : Continuation<T> {
override val context: CoroutineContext = origin.context
override fun resumeWith(result: Result<T>) {
executorService.submit {
// origin.resumeWith 會執行協程中的代碼。在這裏調用的話,協程就
// 能運行在我們自定義的線程池中了
origin.resumeWith(result)
}
}
}
fun close() {
executorService.shutdown()
}
}
fun main() = runBlocking {
val dispatcher = SingleThreadedDispatcher()
log("which thread am I in?")
val job = GlobalScope.launch(dispatcher) {
log("launch before delay")
delay(100)
log("launch after delay")
}
job.join()
dispatcher.close()
}
程序的輸出爲:
[main @coroutine#1] which thread am I in?
[SingleThreadedDispatcher] launch before delay
[SingleThreadedDispatcher] launch after delay
可以看到,它確實運行在我們定義的那個線程池中。
注意到每次調用 executorService.submit
我們都創建了一個新對象,還可以複用 OurContinuation
:
class SingleThreadedDispatcher {
// ...
inner class OurContinuation<T>(private val origin: Continuation<T>)
: Continuation<T>, Runnable {
override val context: CoroutineContext = origin.context
private var result: Result<T>? = null
override fun resumeWith(result: Result<T>) {
this.result = result
executorService.submit(this)
}
override fun run() {
origin.resumeWith(result as Result<T>)
}
}
}
眼尖的你有沒有留意到,雖然我一直在說 Dispatcher,可是前面我們實現的卻是 ContinuationInterceptor
,是 interceptor啊!其實正常情況下,我們一般不會直接來實現 ContinuationInterceptor
,而是實現它的子類 CoroutineDispatcher
,這就是爲什麼我們都說 Dispatcher 而不是 Interceptor。本質上他們的實現和前面的 SingleThreadedDispatcher
是一樣的,讀者可以自行閱讀源碼瞭解一下。
最後一個問題是,在哪裏調用的 interceptContinuation
?答案可以在 suspendCoroutine
的實現裏找到:
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T =
suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
val safe = SafeContinuation(c.intercepted())
block(safe)
safe.getOrThrow()
}
通過 coroutine.intrisics 創建的協程都是 unintercepted 的,一般情況下,我們會調用 intercepted()
方法對它做一個攔截:
public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
(this as? ContinuationImpl)?.intercepted() ?: this
internal abstract class ContinuationImpl {
// ...
public fun intercepted(): Continuation<Any?> =
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
}
編譯器會將 suspend
函數轉成一個 Continuation
,這個 Continuation
就是 ContinuationImpl
;所以最終執行 interceptContinuation
,如果 context 裏有 ContinuationInterceptor
的話。