再談協(xié)程之suspend到底掛起了啥

點(diǎn)擊上方藍(lán)字關(guān)注我,知識會給你力量

Kotlin編譯器會給每一個suspend函數(shù)生成一個狀態(tài)機(jī)來管理協(xié)程的執(zhí)行。
Coroutines簡化了Android上的異步操作。正如文檔中所解釋的,我們可以用它們來管理異步任務(wù),否則可能會阻塞主線程,導(dǎo)致你的應(yīng)用程序Crash。
Coroutines也有助于用命令式的代碼取代基于回調(diào)的API。
作為例子,我們先看看這個使用回調(diào)的異步代碼。
// Simplified code that only considers the happy path
fun loginUser(userId: String, password: String, userResult: Callback<User>) {
// Async callbacks
userRemoteDataSource.logUserIn { user ->
// Successful network request
userLocalDataSource.logUserIn(user) { userDb ->
// Result saved in DB
userResult.success(userDb)
}
}
}
這些回調(diào)可以使用coroutines轉(zhuǎn)換為順序的函數(shù)調(diào)用。
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
在coroutines代碼中,我們給函數(shù)添加了suspend修飾符。這將告訴編譯器,這個函數(shù)需要在一個coroutine內(nèi)執(zhí)行。作為一個開發(fā)者,你可以把suspend函數(shù)看作是一個普通的函數(shù),但它的執(zhí)行可能被掛起,并在某個時候恢復(fù)。
?簡而言之,suspend就是一種編譯器生成的回調(diào)。
?
與回調(diào)不同的是,coroutines提供了一種在線程之間切換和處理異常的簡單方法。
但是,當(dāng)我們把函數(shù)標(biāo)記為suspend時,編譯器實(shí)際上在幕后做了什么?
Suspend到底做了什么
回到loginUser的suspend函數(shù),注意它調(diào)用的其他函數(shù)也是suspend函數(shù)。
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
// UserRemoteDataSource.kt
suspend fun logUserIn(userId: String, password: String): User
// UserLocalDataSource.kt
suspend fun logUserIn(userId: String): UserDb
簡而言之,Kotlin編譯器將使用有限狀態(tài)機(jī)(我們將在后面介紹)把suspend函數(shù)轉(zhuǎn)換為優(yōu)化版本的回調(diào)實(shí)現(xiàn)。你說對了,編譯器會幫你寫這些回調(diào),它們的本質(zhì),依然是回調(diào)!
Continuation的真面目
suspend函數(shù)之間的通信方式是使用Continuation對象。一個Continuation只是一個帶有一些額外信息的通用回調(diào)接口。正如我們稍后將看到的,它將代表一個suspend函數(shù)的生成狀態(tài)機(jī)。
讓我們看一下它的定義。
interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(value: Result<T>)
}
context是在continuation中使用的CoroutineContext。 resumeWith用一個Result來恢復(fù)Coroutine的執(zhí)行,這個Result可以包含一個導(dǎo)致suspend的計算結(jié)果的值或者是一個異常。
?注意:從Kotlin 1.3開始,你還可以使用擴(kuò)展函數(shù)resume(value: T)和resumeWithException(exception: Throwable),它們是resumeWith調(diào)用的特殊版本。
?
編譯器將使用函數(shù)簽名中的額外參數(shù)completion(Continuation類型)替換suspend修飾符,該參數(shù)將用于將suspend函數(shù)的結(jié)果傳達(dá)給調(diào)用它的coroutine。
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
completion.resume(userDb)
}
為了簡單起見,我們的例子將返回Unit而不是User。User對象將在添加的Continuation參數(shù)中被 "返回"。
suspend函數(shù)的字節(jié)碼實(shí)際上返回 Any? 因?yàn)樗?(T | COROUTINE_SUSPENDED)的聯(lián)合類型。這允許函數(shù)在可以時同步返回。
?注意:如果你用suspend修飾符標(biāo)記一個不調(diào)用其他suspend函數(shù)的函數(shù),編譯器也會添加額外的Continuation參數(shù),但不會對它做任何事情,函數(shù)體的字節(jié)碼看起來就像一個普通函數(shù)。
?
你也可以在其他地方看到Continuation接口。
當(dāng)使用suspendCoroutine或suspendCancellableCoroutine將基于回調(diào)的API轉(zhuǎn)換為coroutine時(你應(yīng)該總是傾向于使用這種方法),你直接與Continuation對象交互,以恢復(fù)在運(yùn)行時被suspend的作為參數(shù)傳遞的代碼塊。 你可以使用suspend函數(shù)上的startCoroutine擴(kuò)展函數(shù)來啟動一個coroutine。它接收一個Continuation對象作為參數(shù),當(dāng)新的coroutine完成時,無論是結(jié)果還是異常,都會被調(diào)用。
切換不同的Dispatchers
你可以在不同的Dispatchers之間進(jìn)行交換,在不同的線程上執(zhí)行計算。那么Kotlin如何知道在哪里恢復(fù)一個暫停的計算?
Continuation有一個子類型,叫做DispatchedContinuation,它的resume函數(shù)可以對CoroutineContext中可用的Dispatcher進(jìn)行調(diào)度調(diào)用。除了Dispatchers.Unconfined的isDispatchNeeded函數(shù)覆蓋(在dispatch之前調(diào)用)總是返回false,所有Dispatcher都會調(diào)用dispatch。
在協(xié)程中,有個不成文的約定,那就是,suspend函數(shù)默認(rèn)是不阻塞線程的,也就是說,suspend函數(shù)的調(diào)用者,不用為suspend函數(shù)運(yùn)行在哪個線程而擔(dān)心,suspend函數(shù)會自己處理它工作的線程,不大部分時候,都是通過withContext來進(jìn)行切換的。
生成狀態(tài)機(jī)
?免責(zé)聲明:文章其余部分所展示的代碼將不完全符合編譯器所生成的字節(jié)碼。它將是足夠準(zhǔn)確的Kotlin代碼,使你能夠理解內(nèi)部真正發(fā)生的事情。這種表示法是由Coroutines 1.3.3版本生成的,在該庫的未來版本中可能會發(fā)生變化。
?
Kotlin編譯器將識別函數(shù)何時可以在內(nèi)部suspend。每個suspend point都將被表示為有限狀態(tài)機(jī)中的一個狀態(tài)。這些狀態(tài)由編譯器用標(biāo)簽表示,前面示例中的suspend函數(shù)在編譯后,會產(chǎn)生類似下面的偽代碼。
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
// Label 0 -> first execution
val user = userRemoteDataSource.logUserIn(userId, password)
// Label 1 -> resumes from userRemoteDataSource
val userDb = userLocalDataSource.logUserIn(user)
// Label 2 -> resumes from userLocalDataSource
completion.resume(userDb)
}
為了更好地表示狀態(tài)機(jī),編譯器將使用一個when語句來實(shí)現(xiàn)不同的狀態(tài)。
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
when(label) {
0 -> { // Label 0 -> first execution
userRemoteDataSource.logUserIn(userId, password)
}
1 -> { // Label 1 -> resumes from userRemoteDataSource
userLocalDataSource.logUserIn(user)
}
2 -> { // Label 2 -> resumes from userLocalDataSource
completion.resume(userDb)
}
else -> throw IllegalStateException(...)
}
}
?編譯器將suspend函數(shù)編譯成帶有Continuation參數(shù)的方法叫做CPS(Continuation-Passing-Style)變換。
?
這段代碼是不完整的,因?yàn)椴煌臓顟B(tài)沒有辦法分享信息。編譯器會在函數(shù)中使用相同的Continuation對象來做這件事。這就是為什么Continuation的泛型是Any? 而不是原始函數(shù)的返回類型(即User)。
此外,編譯器將創(chuàng)建一個私有類,1)持有所需的數(shù)據(jù),2)遞歸地調(diào)用loginUser函數(shù)以恢復(fù)執(zhí)行。你可以看看下面這個生成的類的近似值。
?免責(zé)聲明:注釋不是由編譯器生成的。我添加它們是為了解釋它們的作用,并使跟隨代碼更容易理解。
?
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
class LoginUserStateMachine(
// completion parameter is the callback to the function
// that called loginUser
completion: Continuation<Any?>
): CoroutineImpl(completion) {
// Local variables of the suspend function
var user: User? = null
var userDb: UserDb? = null
// Common objects for all CoroutineImpls
var result: Any? = null
var label: Int = 0
// this function calls the loginUser again to trigger the
// state machine (label will be already in the next state) and
// result will be the result of the previous state's computation
override fun invokeSuspend(result: Any?) {
this.result = result
loginUser(null, null, this)
}
}
...
}
由于invokeSuspend將僅用Continuation對象的信息來再次調(diào)用loginUser,loginUser函數(shù)簽名中的其余參數(shù)都變成了空值。在這一點(diǎn)上,編譯器只需要添加如何在狀態(tài)之間轉(zhuǎn)移的信息。
它需要做的第一件事是知道1)這是函數(shù)第一次被調(diào)用,或者2)函數(shù)已經(jīng)從之前的狀態(tài)恢復(fù)。它通過檢查傳入的continuation是否是LoginUserStateMachine類型來實(shí)現(xiàn)。
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
...
}
如果是第一次,它將創(chuàng)建一個新的LoginUserStateMachine實(shí)例,并將收到的完成實(shí)例作為一個參數(shù)存儲起來,這樣它就能記住如何恢復(fù)調(diào)用這個實(shí)例的函數(shù)。如果不是這樣,它將只是繼續(xù)執(zhí)行狀態(tài)機(jī)(suspend函數(shù))。
現(xiàn)在,讓我們看看編譯器為在狀態(tài)間移動和在狀態(tài)間共享信息而生成的代碼。
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
0 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Next time this continuation is called, it should go to state 1
continuation.label = 1
// The continuation object is passed to logUserIn to resume
// this state machine's execution when it finishes
userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
}
1 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Gets the result of the previous state
continuation.user = continuation.result as User
// Next time this continuation is called, it should go to state 2
continuation.label = 2
// The continuation object is passed to logUserIn to resume
// this state machine's execution when it finishes
userLocalDataSource.logUserIn(continuation.user, continuation)
}
... // leaving out the last state on purpose
}
}
花點(diǎn)時間瀏覽一下上面的代碼,看看你是否能發(fā)現(xiàn)與前面的代碼片斷的不同之處。讓我們看看編譯器生成了什么。
when語句的參數(shù)是LoginUserStateMachine實(shí)例中的Label。 每次處理一個新的狀態(tài)時,都會有一個檢查,以防這個函數(shù)suspend時發(fā)生異常。 在調(diào)用下一個suspend函數(shù)(即logUserIn)之前,LoginUserStateMachine實(shí)例的Label將被更新為下一個狀態(tài)。 當(dāng)在這個狀態(tài)機(jī)內(nèi)部有一個對另一個suspend函數(shù)的調(diào)用時,continuation的實(shí)例(LoginUserStateMachine類型)被作為一個參數(shù)傳遞。要調(diào)用的suspend函數(shù)也已經(jīng)被編譯器轉(zhuǎn)化了,它是另一個像這樣的狀態(tài)機(jī),它把一個continuation對象也作為參數(shù)!當(dāng)那個suspend函數(shù)的狀態(tài)機(jī)完成后,它將恢復(fù)這個狀態(tài)機(jī)的執(zhí)行。
最后一個狀態(tài)是不同的,因?yàn)樗仨毣謴?fù)調(diào)用這個函數(shù)的執(zhí)行,正如你在代碼中看到的,它對存儲在LoginUserStateMachine中的cont變量(在構(gòu)造時)調(diào)用resume。
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
...
2 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Gets the result of the previous state
continuation.userDb = continuation.result as UserDb
// Resumes the execution of the function that called this one
continuation.cont.resume(continuation.userDb)
}
else -> throw IllegalStateException(...)
}
}
正如你所看到的,Kotlin編譯器為我們做了很多事情!從這個suspend函數(shù)功能來舉例。
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
編譯器為我們生成了下面這一切。
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
class LoginUserStateMachine(
// completion parameter is the callback to the function that called loginUser
completion: Continuation<Any?>
): CoroutineImpl(completion) {
// objects to store across the suspend function
var user: User? = null
var userDb: UserDb? = null
// Common objects for all CoroutineImpl
var result: Any? = null
var label: Int = 0
// this function calls the loginUser again to trigger the
// state machine (label will be already in the next state) and
// result will be the result of the previous state's computation
override fun invokeSuspend(result: Any?) {
this.result = result
loginUser(null, null, this)
}
}
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
0 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Next time this continuation is called, it should go to state 1
continuation.label = 1
// The continuation object is passed to logUserIn to resume
// this state machine's execution when it finishes
userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
}
1 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Gets the result of the previous state
continuation.user = continuation.result as User
// Next time this continuation is called, it should go to state 2
continuation.label = 2
// The continuation object is passed to logUserIn to resume
// this state machine's execution when it finishes
userLocalDataSource.logUserIn(continuation.user, continuation)
}
2 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Gets the result of the previous state
continuation.userDb = continuation.result as UserDb
// Resumes the execution of the function that called this one
continuation.cont.resume(continuation.userDb)
}
else -> throw IllegalStateException(...)
}
}
Kotlin編譯器將每個suspend函數(shù)轉(zhuǎn)化為一個狀態(tài)機(jī),在每次函數(shù)需要suspend時使用回調(diào)進(jìn)行優(yōu)化。
現(xiàn)在你知道了編譯器在編譯時到底做了什么,你就可以更好地理解為什么一個suspend函數(shù)在它執(zhí)行完所有工作之前不會返回。另外,你也會知道,代碼是如何在不阻塞線程的情況下進(jìn)行suspend的——這是因?yàn)?,?dāng)函數(shù)恢復(fù)時需要執(zhí)行的信息被存儲在Continuation對象中!
參考資料:https://medium.com/androiddevelopers/the-suspend-modifier-under-the-hood-b7ce46af624f
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點(diǎn)擊原文一鍵直達(dá)
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點(diǎn)個“三連”支持一下??
