天天看點

Android開發者快速上手Kotlin(五) 之 協程文法初步12 協程文法初步

接《Android開發者快速上手Kotlin(四) 之 泛型、反射、注解和正則》文章繼續。

12 協程文法初步

12.1簡介

協程(Coroutine)并非什麼新産物,它是幾十年前就已存在的概念,但興起于近些年。Kotlin作為一門朝陽語言,它跟其它近些年新興語言如:go、Lua、python等,一樣都引入了協程的文法支援。

在Java并不存在協程的文法,我們在過去使用Java開發過程中,若想要使用異步邏輯一般需要傳入一個回調接口,待異步邏輯執行完後通過回調接口進行結果傳回。

而協程可以認為它是傳統線程模型的進化版,它可以由程式自行控制挂起和恢複;可以實作多任務的協作執行;可以解決異步任務控制流的靈活轉移進而降低異步程式的設計複雜度。

還是不明白協程是什麼?那我們用最簡單明了的一句話來總結協程吧,協程沒有異步的能力,但能讓異步邏輯使用同步寫法。

12.2 協程與線程的對比

線程:

1.一個程序可以擁有多個線程;

2.線程由系統排程,線程切換或阻塞開銷較大;

3.線程看起來像是語言層次,但實質上和程序一樣是作業系統級的産物,被作業系統核心所管理,隻是通過API暴露給開發者使用;

4.線程之間是搶占式的排程,線程一旦開始執行,從任務的角度來看,就不會被暫停,直到任務結束這個過程都是連續的;

5.線程阻塞是會阻塞目前線程,并且空耗CPU時間而不能執行其它任務。

協程:

1.一個線程可以擁有多個協程;

2.協程依賴于線程,協程挂起時不會阻塞線程,幾乎不存在開銷;

3.協程是編譯器級的魔術,是語言層次的文法,通過插入相關的代碼使得代碼段能夠實作分段式的執行,完全是由程式所控制;

4.協程是非搶占式,是協作式的,是以需要自己釋放使用權來切換到其他協程;

5.協程挂起不會阻塞線程,可以去執行其它計算任務,比如其它協程,這也是我們看到協程實作異步的效果。

12.3 協程的使用入門

概念看了數遍還是很懵逼?這正常不過,要學習一個新東西時,有什麼比一個不加任何修飾的Demo來的直覺呢,基礎的代碼是最好的快速掌握學習的辦法。

12.3.1 Demo代碼

fun main() {
    log("Main函數開始")
    coroutineDo {
        val result = blockFun()
        log("異步方法傳回結果:${result}")
        result                                                                  // 表達式最後一行是傳回值
    }
    log("Main函數結束")
}

fun <T>coroutineDo(block: suspend () -> T) {
    block.startCoroutine(object : Continuation<T> {                             // 建立并啟動協程
        override val context: CoroutineContext = EmptyCoroutineContext          // 協程上下文,如不作處理使用EmptyCoroutineContext即可
        override fun resumeWith(result: Result<T>) {                            // 協程結果統一處理
            log("收到異步結果:${result}")
        }
    })
}

suspend fun blockFun() = suspendCoroutine<String> { continuation ->
    Thread {                                                                    // 協程沒有異步的能力,是以耗時操作依然線上程中完成
        log("異步開始")
        Thread.sleep(2000)
        try {
            continuation.resumeWith(Result.success("異步請求成功"))              // 異步成功的傳回
        } catch (e: Exception) {
            continuation.resumeWith(Result.failure(e))                          // 異步失敗的傳回
        }
    }.start()
}

fun log(msg: String) {
    println("【${Thread.currentThread().name}】${msg}")
}
           

12.3.2 運作結果

程式運作的結果是這樣:

【main】Main函數開始

【main】Main函數結束

【Thread-0】異步開始

【Thread-0】異步方法傳回結果:異步請求成功

【Thread-0】收到異步結果:Success(異步請求成功)
           

補充,如果上面代碼中,将Thread線程去掉,運作的結果會發生順序上的變化(下面會解釋):

【main】Main函數開始

【main】異步開始

【main】異步方法傳回結果:異步請求成功

【main】收到異步結果:Success(異步請求成功)

【main】Main函數結束
           

12.3.3 解說

是不是在看完上面的運作結果,你應該最疑惑的是這一句代碼吧:val result = blockFun(),為什麼blockFun函數裡是一個線程,它沒有傳回值,而在外邊可以直接指派給result變量?不着急,我們來一起一句句地解說。代碼中無非就是定義了兩個關鍵的函數:coroutineDo和blockFun。

一、coroutineDo函數是一個高階函數,因為它的接收參數也是一個函數,而且這個參數是一個“suspend () -> T”類型,代表接收的是一個挂起函數,該挂起函數又傳回了T類型。coroutineDo函數内接收到一個挂起函數後調用其startCoroutine函數,該函數接收一個Continuation對象。

Continuation是協程裡一個關鍵的接口,源碼如下。

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

它是用于運作控制,負責正确的結果和異常的傳回。它有兩個成員:CoroutineContext也是一個接口,表示運作上下文,用于資源持有、運作排程,如果不對它作處理可以給它賦于EmptyCoroutineContext ; resumeWith函數就是用于接收協程裡傳回成功或失敗的結果了。

startCoroutine用于進行協程的建立和啟動,源碼如下:

public fun <T> (suspend () -> T).createCoroutine(completion: Continuation<T>): Continuation<Unit> =
    SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)

public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
    createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}
           

startCoroutine相當于createCoroutine+ resume。createCoroutine函數是建立協程,它接收了一個Continuation,然後再傳回了一個Continuation,resume就是啟動協程。

二、bloclFun函數前面有關鍵字suspend修飾,表示是一個挂起函數,挂起函數隻能在其它挂起函數或協程中被調用(因為它實際會轉化成需要一個Continuation<T>參數的函數)。挂起函數調用的地方叫作挂起點,表示協程挂起,在IDE中代碼行号旁邊會出現這個

Android開發者快速上手Kotlin(五) 之 協程文法初步12 協程文法初步

符号,函數裡通過Continuation.resumeWith(或者使用Continuation.resume和Continuation.resumeWithException)函數來傳回結果,表示協程恢複。

suspend修飾的挂起函數實質上在轉化過程中會多出一個Continuation<T>的參數,但是我們看不到,也不需要自己傳入,這個Continuation參數對象,便是我們在startCoroutine中建立的(注意createCoroutine是接收一個Coroutine傳回一個Coroutine,這個Continuation對象是傳回的那個),然後再通過這個Continuation對象再轉化成一個SafeContinuation對象。其實我們從使用代碼中也能發現suspend函數實際是指向于suspendCoroutine函數,suspendCoroutine接收“(Continuation<T>) -> Unit”類型的函數參數,傳回了T。源碼如下。

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()
    }
           

也就是我們通過suspendCoroutine函數便能擷取到Continuation(SafeContinuation)對象,并對函數内進行Continuation. resumeWith的調用。

是以整個過程中我們會發現存在3個Continuation:第1個是我們調用startCoroutine傳入的,第2個是createCoroutine方法接收第1個後生成傳回的,第3個是suspendCoroutine裡轉化的SafeContinuation。

請注意和記往:suspendCoroutine這裡有一個很有意思的情況,如果suspend函數中并沒存線上程的切換,則表示并沒有真正的挂起,則會直接傳回T,而如果存在挂起情況時,就會通過Continuation來傳回。是以上面的Demo中,如果你嘗試将Thread注釋的話,程式的運作結果會按正常代碼順序執行。

12.3.4 簡化

如果你已經了解完上面的解說的話,上述Demo中的兩個關鍵的函數:coroutineDo和blockFun還可以去除掉,代碼可以這樣簡化:

fun main() {
    log("Main函數開始")
    suspend {
        val result = suspendCoroutine<String> { continuation ->
            Thread {
                log("異步開始")
                Thread.sleep(2000)
                try {
                    continuation.resumeWith(Result.success("異步請求成功"))
                } catch (e: Exception) {
                    continuation.resumeWith(Result.failure(e))
                }
            }.start()
        }
        log("異步方法傳回結果:${result}")
        result
    }.startCoroutine(object : Continuation<String> {
        override val context: CoroutineContext = EmptyCoroutineContext
        override fun resumeWith(result: Result<String>) {
            log("收到異步結果:${result}")
        }
    })
    log("Main函數結束")
}
fun log(msg: String) {
    println("【${Thread.currentThread().name}】$msg")
}
           

12.4 補充:協程的分類

12.4.1 協程按調用棧分類

有棧協程:每個協程會配置設定單獨的調用棧,類似線程的調用棧,可以通過棧來儲存局部變量。可以在任意函數嵌套中挂起。例如 Lua的協程就是有棧式協程

無棧協程:不會配置設定單獨的調用棧,挂起點狀态通過閉包或對象儲存。隻能在目前函數中挂起。例如 Kotlin和Python都是一種無棧協程

12.4.2 協程按調用關系分類

對稱協程:排程權可以轉移給任意協程,協程之間是對等關系。對稱協程的方式更像在執行多個互相獨立的任務并發。

非對稱協程:排程權隻能轉移給調用自己的協程,協程存在父子關系。大多實作都是非對稱協程。

未完,請關注後面文章更新…

繼續閱讀