iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 9

day9 Kotlin coroutine 的黑魔法 suspend

suspend他並不能切換線程,切換線程的是內部自帶的suspend函數,ex. withContext

coroutine只是能用阻塞寫法寫出非阻塞代碼,本質和thread是一樣的

掛起函數?

剛接觸coroutine的人應該都會遇到這個名詞,掛起函數,蛤?
掛起?掛起誰,掛在哪裡,什麼時候掛起,讓我一個一個回答

  1. 掛起誰? 掛起coroutine
  2. 從哪掛起? 從當前的thread掛起,當thread執行到一個suspend之後,執行掛起,並結束工作,結束工作後他要幹嘛? main thread就去繪製ui,io去執行其他io任務,如果沒任務就等著被回收
  3. 什麼時候掛起? 耗時任務或調用另一個suspend function

我們前面講過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的功用,語法角度

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(),或其他方式確保主線程安全

suspend背後做什麼 -- resume

前面講過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(...)
    }
}

suspend under the hood

suspend and resume

所以掛起了, 就不塞了?

大家都說,coroutine的掛起是非阻塞式的,真有那麼神奇的黑魔法嗎?

他的非阻塞式,是指不卡thread

在我們學coroutine的漫漫長路裡,一定會有文章說,coroutine是非阻塞式,thread是阻塞式,對,但也不對,因為他沒講明白,記得一點,kotlin 的coroutine是線程框架,它的本質是一樣的,thread的切換也是非阻塞式

那為什麼又說他對呢?

以單個thread來說,耗時任務是阻塞式的,那一單個coroutine來說呢,它可以是非阻塞式的,因為他能用suspend來切換線程,懂了嗎? coroutine是切換thread來達到非阻塞,那能不能用thread寫非阻塞代碼,當然可以,因為要做的事都一樣,就是切換thread

coroutine只是能用阻塞寫法寫出非阻塞代碼

扔物線影片

那suspend有比較高效嗎?

以開發時間來看,有的
以程式執行來看,沒有

為甚麼呢,不是說掛起後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的話,他是會照順序完成的。

連結統整

必看

扔物線影片
suspend under the hood

suspend and resume


上一篇
day8 kotlin coroutine的 runBlocking, withContext
下一篇
聊聊structure concurrency 結構化併發
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30

尚未有邦友留言

立即登入留言