Kotlin協(xié)程實現(xiàn)原理:Suspend&CoroutineContext

今天我們來聊聊Kotlin的協(xié)程Coroutine

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

如果你已經(jīng)接觸過協(xié)程呀舔,相信你都有過以下幾個疑問:

  1. 協(xié)程到底是個什么東西?
  2. 協(xié)程的suspend有什么作用扩灯,工作原理是怎樣的媚赖?
  3. 協(xié)程中的一些關(guān)鍵名稱(例如:JobCoroutine珠插、Dispatcher惧磺、CoroutineContextCoroutineScope)它們之間到底是怎么樣的關(guān)系?
  4. 協(xié)程的所謂非阻塞式掛起與恢復(fù)又是什么丧失?
  5. 協(xié)程的內(nèi)部實現(xiàn)原理是怎么樣的豺妓?
  6. ...

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

協(xié)程是什么

這個疑問很簡單琳拭,只要你不是野路子接觸協(xié)程的,都應(yīng)該能夠知道描验。因為官方文檔中已經(jīng)明確給出了定義白嘁。

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

協(xié)程是一種并發(fā)設(shè)計模式膘流,您可以在 Android 平臺上使用它來簡化異步執(zhí)行的代碼絮缅。

敲黑板劃重點:協(xié)程是一種并發(fā)的設(shè)計模式。

所以并不是一些人所說的什么線程的另一種表現(xiàn)呼股。雖然協(xié)程的內(nèi)部也使用到了線程耕魄。但它更大的作用是它的設(shè)計思想。將我們傳統(tǒng)的Callback回調(diào)方式進行消除彭谁。將異步編程趨近于同步對齊吸奴。

解釋了這么多,最后我們還是直接點缠局,來看下它的優(yōu)點

  1. 輕量:您可以在單個線程上運行多個協(xié)程则奥,因為協(xié)程支持掛起,不會使正在運行協(xié)程的線程阻塞狭园。掛起比阻塞節(jié)省內(nèi)存读处,且支持多個并行操作。
  2. 內(nèi)存泄露更少:使用結(jié)構(gòu)化并發(fā)機制在一個作用域內(nèi)執(zhí)行多個操作唱矛。
  3. 內(nèi)置取消支持:取消功能會自動通過正在運行的協(xié)程層次結(jié)構(gòu)傳播罚舱。
  4. Jetpack集成:許多 Jetpack 庫都包含提供全面協(xié)程支持的擴展。某些庫還提供自己的協(xié)程作用域绎谦,可供您用于結(jié)構(gòu)化并發(fā)管闷。

suspend

suspend是協(xié)程的關(guān)鍵字,每一個被suspend修飾的方法都必須在另一個suspend函數(shù)或者Coroutine協(xié)程程序中進行調(diào)用燥滑。

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

當(dāng)然搀菩,如果你有關(guān)注我之前的文章呕臂,應(yīng)該就會有所了解,因為在重溫Retrofit源碼肪跋,笑看協(xié)程實現(xiàn)這篇文章中我已經(jīng)有簡單的提及歧蒋。

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

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

上面這段代碼經(jīng)過CPS轉(zhuǎn)換之后真正的面目是這樣的

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

經(jīng)過轉(zhuǎn)換之后谜洽,原有的返回類型NewsResponse被添加到新增的Continutation參數(shù)中萝映,同時返回了Any?類型。這里可能會有所疑問阐虚?返回類型都變了序臂,結(jié)果不就出錯了嗎?

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

當(dāng)suspend函數(shù)被協(xié)程掛起時咸灿,它會返回一個特殊的標識COROUTINE_SUSPENDED构订,而它本質(zhì)就是一個Any;當(dāng)協(xié)程不掛起進行執(zhí)行時避矢,它將返回執(zhí)行的結(jié)果或者引發(fā)的異常悼瘾。這樣為了讓這兩種情況的返回都支持,所以使用了Kotlin獨有的Any?類型谷异。

返回值搞明白了分尸,現(xiàn)在來說說這個Continutation參數(shù)。

首先來看下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是協(xié)程的上下文歹嘹,它更多時候是CombinedContext類型箩绍,類似于協(xié)程的集合,這個后續(xù)會詳情說明尺上。

resumeWith是用來喚醒掛起的協(xié)程材蛛。前面已經(jīng)說過協(xié)程在執(zhí)行的過程中,為了防止阻塞使用了掛起的特性怎抛,一旦協(xié)程內(nèi)部的邏輯執(zhí)行完畢之后卑吭,就是通過該方法來喚起協(xié)程。讓它在之前掛起的位置繼續(xù)執(zhí)行下去马绝。

所以每一個被suspend修飾的函數(shù)都會獲取上層的Continutation豆赏,并將其作為參數(shù)傳遞給自己。既然是從上層傳遞過來的富稻,那么Continutation是由誰創(chuàng)建的呢掷邦?

其實也不難猜到,Continutation就是與協(xié)程創(chuàng)建的時候一起被創(chuàng)建的椭赋。

GlobalScope.launch { 
             
}

launch的時候就已經(jīng)創(chuàng)建了Continutation對象抚岗,并且啟動了協(xié)程。所以在它里面進行掛起的協(xié)程傳遞的參數(shù)都是這個對象哪怔。

簡單的理解就是協(xié)程使用resumeWith替換傳統(tǒng)的callback宣蔚,每一個協(xié)程程序的創(chuàng)建都會伴隨Continutation的存在向抢,同時協(xié)程創(chuàng)建的時候都會自動回調(diào)一次ContinutationresumeWith方法,以便讓協(xié)程開始執(zhí)行胚委。

CoroutineContext

協(xié)程的上下文挟鸠,它包含用戶定義的一些數(shù)據(jù)集合,這些數(shù)據(jù)與協(xié)程密切相關(guān)篷扩。它類似于map集合兄猩,可以通過key來獲取不同類型的數(shù)據(jù)茉盏。同時CoroutineContext的靈活性很強鉴未,如果其需要改變只需使用當(dāng)前的CoroutineContext來創(chuàng)建一個新的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鸠姨,我們可以通過對應(yīng)的Key來獲取對應(yīng)的具體對象铜秆。說的有點抽象我們直接通過例子來了解。

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]]

Job讶迁、DispatchersCoroutineName都實現(xiàn)了Element接口连茧。

如果需要結(jié)合不同的CoroutineContext可以直接通過+拼接,本質(zhì)就是使用了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的實現(xiàn)邏輯是將兩個拼接的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]
            }
        }
    }
    ...
}

注意看它的兩個參數(shù)祟峦,我們直接拿上面的例子來分析

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

Job對應(yīng)于left罚斗,Dispatchers.IO對應(yīng)element。如果再拼接一層CoroutineName(aa)就是這樣的

((Job, Dispatchers.IO),CoroutineName)

功能類似與鏈表宅楞,但不同的是你能夠拿到上一個與你相連的整體內(nèi)容针姿。與之對應(yīng)的就是minusKey方法,從集合中移除對應(yīng)KeyCoroutineContext實例厌衙。

有了這個基礎(chǔ)距淫,我們再看它的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>喻杈,當(dāng)然這樣還不夠彤枢,需要繼續(xù)結(jié)合對于的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的方式來獲取整個協(xié)程中CoroutineContext集合中對應(yīng)KeyCoroutineContext實例。

本篇文章主要介紹了suspend的工作原理與CoroutineContext的內(nèi)部結(jié)構(gòu)龄砰。希望對學(xué)習(xí)協(xié)程的伙伴們能夠有所幫助盟猖,敬請期待后續(xù)的協(xié)程分析讨衣。

項目

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

AwesomeGithub: 基于Github客戶端,純練習(xí)項目你弦,支持組件化開發(fā)惊豺,支持賬戶密碼與認證登陸。使用Kotlin語言進行開發(fā)禽作,項目架構(gòu)是基于Jetpack&DataBindingMVVM尸昧;項目中使用了ArouterRetrofit旷偿、Coroutine烹俗、GlideDaggerHilt等流行開源技術(shù)萍程。

flutter_github: 基于Flutter的跨平臺版本Github客戶端幢妄,與AwesomeGithub相對應(yīng)。

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末置吓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子缔赠,更是在濱河造成了極大的恐慌衍锚,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗤堰,死亡現(xiàn)場離奇詭異戴质,居然都是意外死亡,警方通過查閱死者的電腦和手機踢匣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門告匠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人离唬,你說我怎么就攤上這事后专。” “怎么了输莺?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵戚哎,是天一觀的道長裸诽。 經(jīng)常有香客問我,道長型凳,這世上最難降的妖魔是什么丈冬? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮甘畅,結(jié)果婚禮上埂蕊,老公的妹妹穿的比我還像新娘。我一直安慰自己疏唾,他們只是感情好蓄氧,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荸实,像睡著了一般匀们。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上准给,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音重抖,去河邊找鬼露氮。 笑死,一個胖子當(dāng)著我的面吹牛钟沛,可吹牛的內(nèi)容都是我干的畔规。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼恨统,長吁一口氣:“原來是場噩夢啊……” “哼叁扫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起畜埋,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤莫绣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后悠鞍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體对室,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年咖祭,在試婚紗的時候發(fā)現(xiàn)自己被綠了掩宜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡么翰,死狀恐怖牺汤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浩嫌,我是刑警寧澤檐迟,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布戴已,位于F島的核電站,受9級特大地震影響锅减,放射性物質(zhì)發(fā)生泄漏糖儡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一怔匣、第九天 我趴在偏房一處隱蔽的房頂上張望握联。 院中可真熱鬧,春花似錦每瞒、人聲如沸金闽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽代芜。三九已至,卻和暖如春浓利,著一層夾襖步出監(jiān)牢的瞬間挤庇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工贷掖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫡秕,地道東北人岸浑。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓馋艺,卻偏偏與公主長得像袁稽,于是被迫代替她去往敵國和親吮螺。 傳聞我的和親對象是個殘疾皇子灾螃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容