接《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中代碼行號旁邊會出現這個符號,函數裏通過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 協程按調用關係分類
對稱協程:調度權可以轉移給任意協程,協程之間是對等關係。對稱協程的方式更像在執行多個相互獨立的任務併發。
非對稱協程:調度權只能轉移給調用自己的協程,協程存在父子關係。大多實現都是非對稱協程。
未完,請關注後面文章更新…