Kotlin 協程到底運行在哪個線程裏

與其說協程是一個輕量級線程,我更願意把它當然一個個待執行/可執行的任務。這樣就引申出一個問題——協程是運行在哪個線程上的?這就是本篇文章想要探討的問題,同時我們也將學習如何讓協程在特定的線程裏執行。

首先來看一個例子:

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

這個輸出有兩個要點:

  1. 從線程名推斷,兩個協程很可能運行在某個線程池中
  2. 第二個協程先運行在 worker-2,然後又運行在 worker-1

關於第一點, launch 的文檔有這麼一句話:

If the context does not have any dispatcher nor any other ContinuationInterceptor, then Dispatchers.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 方法。ContinuationInterceptorinterceptContinuation 可以返回一個新的 Continuation,在這個新的 ContinuationresumeWith 裏面,我們可以讓協程運行在任意的線程裏。

下面我們看一個最簡單的 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 的話。

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