Kotlin協程實現原理:Suspend&CoroutineContext

今天我們來聊聊Kotlin的協程Coroutine

如果你還沒有接觸過協程,推薦你先閱讀這篇入門級文章What? 你還不知道Kotlin Coroutine?

如果你已經接觸過協程,相信你都有過以下幾個疑問:

  1. 協程到底是個什么東西?
  2. 協程的suspend有什么作用,工作原理是怎樣的?
  3. 協程中的一些關鍵名稱(例如:JobCoroutineDispatcherCoroutineContextCoroutineScope)它們之間到底是怎么樣的關系?
  4. 協程的所謂非阻塞式掛起與恢復又是什么?
  5. 協程的內部實現原理是怎么樣的?
  6. ...

接下來的一些文章試著來分析一下這些疑問,也歡迎大家一起加入來討論。

協程是什么

這個疑問很簡單,只要你不是野路子接觸協程的,都應該能夠知道。因為官方文檔中已經明確給出了定義。

下面來看下官方的原話(也是這篇文章最具有底氣的一段話)。

協程是一種并發設計模式,您可以在 Android 平臺上使用它來簡化異步執行的代碼。

敲黑板劃重點:協程是一種并發的設計模式。

所以并不是一些人所說的什么線程的另一種表現。雖然協程的內部也使用到了線程。但它更大的作用是它的設計思想。將我們傳統的Callback回調方式進行消除。將異步編程趨近于同步對齊。

解釋了這么多,最后我們還是直接點,來看下它的優點

  1. 輕量:您可以在單個線程上運行多個協程,因為協程支持掛起,不會使正在運行協程的線程阻塞。掛起比阻塞節省內存,且支持多個并行操作。
  2. 內存泄露更少:使用結構化并發機制在一個作用域內執行多個操作。
  3. 內置取消支持:取消功能會自動通過正在運行的協程層次結構傳播。
  4. Jetpack集成:許多 Jetpack 庫都包含提供全面協程支持的擴展。某些庫還提供自己的協程作用域,可供您用于結構化并發。

suspend

suspend是協程的關鍵字,每一個被suspend修飾的方法都必須在另一個suspend函數或者Coroutine協程程序中進行調用。

第一次看到這個定義不知道你們是否有疑問,反正小憩我是很疑惑,為什么suspend修飾的方法需要有這個限制呢?不加為什么就不可以,它的作用到底是什么?

當然,如果你有關注我之前的文章,應該就會有所了解,因為在重溫Retrofit源碼,笑看協程實現這篇文章中我已經有簡單的提及。

這里涉及到一種機制俗稱CPS(Continuation-Passing-Style)。每一個suspend修飾的方法或者lambda表達式都會在代碼調用的時候為其額外添加Continuation類型的參數。

@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse

上面這段代碼經過CPS轉換之后真正的面目是這樣的

@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): Any?

經過轉換之后,原有的返回類型NewsResponse被添加到新增的Continutation參數中,同時返回了Any?類型。這里可能會有所疑問?返回類型都變了,結果不就出錯了嗎?

其實不是,Any?Kotlin中比較特殊,它可以代表任意類型。

suspend函數被協程掛起時,它會返回一個特殊的標識COROUTINE_SUSPENDED,而它本質就是一個Any;當協程不掛起進行執行時,它將返回執行的結果或者引發的異常。這樣為了讓這兩種情況的返回都支持,所以使用了Kotlin獨有的Any?類型。

返回值搞明白了,現在來說說這個Continutation參數。

首先來看下Continutation的源碼

public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext
 
    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

context是協程的上下文,它更多時候是CombinedContext類型,類似于協程的集合,這個后續會詳情說明。

resumeWith是用來喚醒掛起的協程。前面已經說過協程在執行的過程中,為了防止阻塞使用了掛起的特性,一旦協程內部的邏輯執行完畢之后,就是通過該方法來喚起協程。讓它在之前掛起的位置繼續執行下去。

所以每一個被suspend修飾的函數都會獲取上層的Continutation,并將其作為參數傳遞給自己。既然是從上層傳遞過來的,那么Continutation是由誰創建的呢?

其實也不難猜到,Continutation就是與協程創建的時候一起被創建的。

GlobalScope.launch { 
             
}

launch的時候就已經創建了Continutation對象,并且啟動了協程。所以在它里面進行掛起的協程傳遞的參數都是這個對象。

簡單的理解就是協程使用resumeWith替換傳統的callback,每一個協程程序的創建都會伴隨Continutation的存在,同時協程創建的時候都會自動回調一次ContinutationresumeWith方法,以便讓協程開始執行。

CoroutineContext

協程的上下文,它包含用戶定義的一些數據集合,這些數據與協程密切相關。它類似于map集合,可以通過key來獲取不同類型的數據。同時CoroutineContext的靈活性很強,如果其需要改變只需使用當前的CoroutineContext來創建一個新的CoroutineContext即可。

來看下CoroutineContext的定義

public interface CoroutineContext {
    /**
     * Returns the element with the given [key] from this context or `null`.
     */
    public operator fun <E : Element> get(key: Key<E>): E?

    /**
     * Accumulates entries of this context starting with [initial] value and applying [operation]
     * from left to right to current accumulator value and each element of this context.
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...

    /**
     * Returns a context containing elements from this context, but without an element with
     * the specified [key].
     */
    public fun minusKey(key: Key<*>): CoroutineContext

    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {..}
}

每一個CoroutineContext都有它唯一的一個Key其中的類型是Element,我們可以通過對應的Key來獲取對應的具體對象。說的有點抽象我們直接通過例子來了解。

var context = Job() + Dispatchers.IO + CoroutineName("aa")
LogUtils.d("$context, ${context[CoroutineName]}")
context = context.minusKey(Job)
LogUtils.d("$context")
// 輸出
[JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa)
[CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]

JobDispatchersCoroutineName都實現了Element接口。

如果需要結合不同的CoroutineContext可以直接通過+拼接,本質就是使用了plus方法。

    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

plus的實現邏輯是將兩個拼接的CoroutineContext封裝到CombinedContext中組成一個拼接鏈,同時每次都將ContinuationInterceptor添加到拼接鏈的最尾部.

那么CombinedContext又是什么呢?

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
 
    override fun <E : Element> get(key: Key<E>): E? {
        var cur = this
        while (true) {
            cur.element[key]?.let { return it }
            val next = cur.left
            if (next is CombinedContext) {
                cur = next
            } else {
                return next[key]
            }
        }
    }
    ...
}

注意看它的兩個參數,我們直接拿上面的例子來分析

Job() + Dispatchers.IO
(Job, Dispatchers.IO)

Job對應于leftDispatchers.IO對應element。如果再拼接一層CoroutineName(aa)就是這樣的

((Job, Dispatchers.IO),CoroutineName)

功能類似與鏈表,但不同的是你能夠拿到上一個與你相連的整體內容。與之對應的就是minusKey方法,從集合中移除對應KeyCoroutineContext實例。

有了這個基礎,我們再看它的get方法就很清晰了。先從element中去取,沒有再從之前的left中取。

那么這個Key到底是什么呢?我們來看下CoroutineName

public data class CoroutineName(
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    /**
     * Key for [CoroutineName] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineName>
 
    /**
     * Returns a string representation of the object.
     */
    override fun toString(): String = "CoroutineName($name)"
}

很簡單它的Key就是CoroutineContext.Key<CoroutineName>,當然這樣還不夠,需要繼續結合對于的operator get方法,所以我們再來看下Elementget方法

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

這里使用到了Kotlinoperator操作符重載的特性。那么下面的代碼就是等效的。

context.get(CoroutineName)
context[CoroutineName]

所以我們就可以直接通過類似于Map的方式來獲取整個協程中CoroutineContext集合中對應KeyCoroutineContext實例。

本篇文章主要介紹了suspend的工作原理與CoroutineContext的內部結構。希望對學習協程的伙伴們能夠有所幫助,敬請期待后續的協程分析。

項目

android_startup: 提供一種在應用啟動時能夠更加簡單、高效的方式來初始化組件,優化啟動速度。不僅支持Jetpack App Startup的全部功能,還提供額外的同步與異步等待、線程控制與多進程支持等功能。

AwesomeGithub: 基于Github客戶端,純練習項目,支持組件化開發,支持賬戶密碼與認證登陸。使用Kotlin語言進行開發,項目架構是基于Jetpack&DataBindingMVVM;項目中使用了ArouterRetrofitCoroutineGlideDaggerHilt等流行開源技術。

flutter_github: 基于Flutter的跨平臺版本Github客戶端,與AwesomeGithub相對應。

android-api-analysis: 結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點。

daily_algorithm: 每日一算法,由淺入深,歡迎加入一起共勉。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容