suspend他並不能切換線程,切換線程的是內部自帶的suspend函數,ex. withContext
coroutine只是能用阻塞寫法寫出非阻塞代碼,本質和thread是一樣的
剛接觸coroutine的人應該都會遇到這個名詞,掛起函數,蛤?
掛起?掛起誰,掛在哪裡,什麼時候掛起,讓我一個一個回答
我們前面講過launch/ async實際上做了甚麼,將任務post到thread裡面,對吧?
那今天如果任務是掛起的呢? 那就掛起呀,以範例來說,Main thread執行到suspendThis()的時候,就會結束我們post過去的任務,去做他原本該做的事
ㄟ等等,但我們的任務還沒做完呀?
不急不急,聽我接著講,任務結束,是對Main thread來說結束,而任務本身會透過withContext繼續在IO thread執行,還記得我們說過withContext的特性吧,暫時切換thread,然後再切回去,沒錯,任務結束後,withContext又會自動幫我們切回去,而這所謂的自動切回,就是coroutine會再幫我們post一個任務,讓我們回到原先的thread繼續執行
所謂的掛起函數,就是稍後會被自動切回來的thread切換。 by扔物線
val scope = CoroutineScope(rootJob)
scope.launch {
suspendThis()
Log.i("","")
}
//變成
handler.post{
suspendThis()
Log.i("","")
}
suspend fun suspendThis(){
withContext ( Dispatchers.IO ){
// io task
}
}
隨便找了一張圖,但我不打算講thread,只是給你看剛剛講到的東西
圖源
那要suspend幹嗎? 他又不負責切線程,拿掉不行嗎? 诶~真不行,它的用處,現在才要開始
suspend是個coroutine很常見的關鍵字,幾乎到處都能看到他的身影,標記了suspend的方法,一定要在coroutine或另外的suspend內使用,當調用了supend方法,會暫停當前coroutine的執行,並保留所有局部變量,並在結束後resume,並執行之後的code
suspend — pause the execution of the current coroutine, saving all local variables
resume — continue a suspended coroutine from the place it was paused
在語法方面,suspend本身是提醒開發者,這項任務需要耗時,或是切換thread,請要coroutine裡面適當的調用我,而真正耗時的部分是suspend fun裡面的code
這個提醒,有用嗎?大有用處,我們自己都有可能忘記某個fun是耗時的,更不用說,如果你用了一個package,你也不知道他的代碼是耗時的呀,一不小心,ui卡一下,又要通靈抓bug了,那如果有提醒的話呢?ide會告訴方法的調用者,我是耗時任務,在coroutine裡面調用我
suspend方法並不會讓kotlin在後臺執行函數,在主線程使用suspend或是啟動協程是相當常見的,而我們應該使用withContext(),或其他方式確保主線程安全
前面講過callback沒有不見,而是編譯器透過finite state machine將suspend fun轉換為callback的版本
TL;DR; The Kotlin compiler will create a state machine for every suspend function that manages the coroutine’s execution for us!
我們切線程再切回來,有個關鍵字叫resume,用中文理解一下就是恢復狀態
而他怎麼恢復狀態呢? 他是透過coroutine的 Continuation ,來達到恢復狀態,這也是為甚麼suspend函數只能在coroutineScope或另一個suspend裡面執行,因為要用coroutine才能達到恢復狀態,對吧?
那Continuation 又是甚麼? 官方解釋說他是帶額外訊息的回調接口,source code長這樣
interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(value: Result<T>)
}
看這裡了解CoroutineContext
resumeWith,用Result回復coroutine的執行,可能包含執行結果或是Exception
那suspend編譯後長怎樣
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
completion.resume(userDb)
}
這裡的completion非常重要,他是用來將suspend 的結果回傳給調用他的coroutine,但這只是簡化版的code
直接跟官方blog借code,我有附連結在下面,蠻建議去讀的,這邊我就簡單帶一下概念而已
suspend透過Continuation在不同suspend切換thread之間,傳遞value,並且透過轉型將Continuation轉換成 StateMachine 類別,利用label確定執行順序,如果是第一次執行fun,會建立State machine,之後每次都會將State Machine作為參數傳遞,遞迴呼叫loginUser function
直到最後,透過resume回傳了userDb,可以對應上面的code
/* 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(...)
}
}
大家都說,coroutine的掛起是非阻塞式的,真有那麼神奇的黑魔法嗎?
他的非阻塞式,是指不卡thread
在我們學coroutine的漫漫長路裡,一定會有文章說,coroutine是非阻塞式,thread是阻塞式,對,但也不對,因為他沒講明白,記得一點,kotlin 的coroutine是線程框架,它的本質是一樣的,thread的切換也是非阻塞式
那為什麼又說他對呢?
以單個thread來說,耗時任務是阻塞式的,那一單個coroutine來說呢,它可以是非阻塞式的,因為他能用suspend來切換線程,懂了嗎? coroutine是切換thread來達到非阻塞,那能不能用thread寫非阻塞代碼,當然可以,因為要做的事都一樣,就是切換thread
coroutine只是能用阻塞寫法寫出非阻塞代碼
以開發時間來看,有的
以程式執行來看,沒有
為甚麼呢,不是說掛起後thread就能執行其他任務嗎? 這樣不就不用痴痴等待
複習一下前面的概念,任務執行是cpu的工作,今天我們要執行io請求,要切到io thread對吧?
注意,一個耗時任務,是會慢慢執行,而不是在某個時間點突然完成,以前面範例來說,我們讓main thread掛起耗時任務去執行其他任務,為甚麼? 因為在main執行耗時任務ui會freeze,會ANR對吧?
那任務不做了嗎? 要做呀,只是我們拿到io thread執行了呀,要做的事情一件都沒有少
那suspend不是能掛起嗎? 那在io thread掛起,不就可以提升thread的利用率,這種想法很誘人,卻很誤導,也非常危險
前面我們講了什麼? 掛起的任務是對當前的thread來說,這個coroutine結束了,並且在suspend function執行完畢後,切回原本的thread,往裡面post之後的任務,那是不是還是要有thread去完成任務,畢竟他不會通靈,也不會自己完成呀
那能不能在一個thread裡面做併發呢?
io/ default可以有複數的thread,os會從thread pool拿出thread來執行任務,任務完成後要嘛回收要嘛再利用; 還有一點,async的併發會創造新的coroutine,是透過不同coroutine在不同 thread同時做多個任務,如果你的併發把耗時任務丟到Main thread的話,他是會照順序完成的。