kotlin 協程上下文那點事

用線程做類比的話,協程的 context 可以認爲是協程的“線程私有變量”,同時這個私有變量是不可變的。也就是說,我們在創建一個協程的時候,他的 context 攜帶的信息就已經確定了下來。由於協程是很輕量的,在需要改變協程上下文的時候,我們可以直接新創建一個協程,所以不可變性不算是一個大問題。

它的兩個跟我們我們日常開發聯繫比較大的方法如下:

@SinceKotlin("1.3")
public interface CoroutineContext {
	interface Key<E : Element>
	
	public operator fun <E : Element> get(key: Key<E>): E?
	public operator fun plus(context: CoroutineContext): CoroutineContext
	
	// 注:省略了 fold 和 minusKey
}

context 可以認爲是一個 map,通過它的 get 方法可以取出集合裏的內容,比如:

val interceptor: ContinuationInterceptor? = context[ContinuationInterceptor]

interceptor 我們下篇文章再聊。這裏用來做索引的 ContinuationInterceptor 其實是類 ContinuationInterceptor 的 companion object,並且這個伴生對象繼承了 CoroutineContext.Key<ContinuationInterceptor>。初看起來這種寫法有點奇怪,但習慣了以後還是不得不承認這個是很優雅的設計(相當於一個協變類型的 map)。下面我們看看如何給 context 帶上自定義的信息。

Element 是集合裏的內容:

interface CoroutineContext {
	// ...

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {
        /**
         * A key of this coroutine context element.
         */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null
    }
}

實現它很簡單:

class CoroutineName(val name: String) : CoroutineContext.Element {
	// companion object 是一個類對象,理所應當的,它能繼承其他類
    companion object : CoroutineContext.Key<CoroutineName>
    override val key = CoroutineName
}

fun main() = runBlocking {
    GlobalScope.launch(Dispatchers.IO + CoroutineName("my coroutine")) {
    	// CoroutineName 的 companion object 繼承的是 Key<CoroutineName>,
    	// 所以 coroutineContext[CoroutineName] 的類型是 CoroutineName?
        val name = coroutineContext[CoroutineName]?.name ?: "no name"
        println("name = $name")
    }
    Unit
}
// 打印:
// name = my coroutine

// 或者更常見的:
// 這裏的 + 使用的是 CoroutineContext 裏定義的 operator plus
someCoroutineScope.launch(Dispatchers.IO + CoroutineName("my coroutine")) {
    val name = coroutineContext[CoroutineName]?.name ?: return
    // ...
}

由於這種定義 context 是固定的模型,kotlin 還提供了一個便捷類:

class CoroutineName(val name: String) : AbstractCoroutineContextElement(CoroutineName) {
    companion object : CoroutineContext.Key<CoroutineName>
}

關於協程上下文,最後還剩下的問題是,我們如何拿到它。其中一種方法在上面的例子中我們已經看到:

public interface CoroutineScope {
    /**
     * The context of this scope.
     */
    public val coroutineContext: CoroutineContext
}

傳遞給 CoroutineScope.launch() 的 lambda 有一個類型爲 CoroutineScope 的 receiver,這就是前面我們能夠直接使用 coroutineContext 的原因。

另一個獲取 context 的途徑是 Continuation,例如:

interface Callback {
    fun onResult(enabled: Boolean)
}
fun isFeatureXEnabled(callback: Callback) {
    // ...
}

suspend fun isFeatureXEnabled() = suspendCoroutine { conn: Continuation<Boolean> ->
    isFeatureXEnabled(object : Callback {
        override fun onResult(enabled: Boolean) {
            val context = conn.context
            conn.resume(enabled)
        }
    })
}

Continuationcontext 字段就是 CoroutineContext

@SinceKotlin("1.3")
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

關於協程上下文的內容差不多就是這些了,如果讀者有想分享出來的內容,歡迎給我留言。

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