Kotlin協程實現原理:Suspend&CoroutineContext

1_osc1NdwrqJjJuGR1XZsmlg.jpeg

今天咱們來聊聊Kotlin的協程Coroutineandroid

若是你尚未接觸過協程,推薦你先閱讀這篇入門級文章What? 你還不知道Kotlin Coroutine?git

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

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

接下來的一些文章試着來分析一下這些疑問,也歡迎你們一塊兒加入來討論。算法

協程是什麼

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

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

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

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

因此並非一些人所說的什麼線程的另外一種表現。雖然協程的內部也使用到了線程。但它更大的做用是它的設計思想。將咱們傳統的Callback回調方式進行消除。將異步編程趨近於同步對齊。併發

解釋了這麼多,最後咱們仍是直接點,來看下它的優勢app

  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: 每日一算法,由淺入深,歡迎加入一塊兒共勉。

相關文章
相關標籤/搜索