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


<<:  Day 9 - 实现社交工程

>>:  D3JsDay09 资料元素来绑定,让你元素有内定—资料绑定

Day 17 Compose Gestures partI

今年的疫情蛮严重的,希望大家都过得安好,希望疫情快点过去,能回到一些线下技术聚会的时光~ 今天目标:...

Day 7 - 用 canvas 复刻 小画家 绘制矩形与圆角矩形

绘制矩形 核心 先来学习绘制矩形的方法 strokeRect 使用当前的绘画样式,描绘一个起点在 (...

SQL与NoSQL的连结(二)

接续前次实作. 由於资料转换需要透过一个 instance 运作, 先建立 Replication ...

使用Emmet省下泡一杯咖啡的时间(HTML篇)

使用Emmet省下泡一杯咖啡的时间 (HTML篇) 效率满点的好工具 为什麽而学? 前身是Zen c...

【Day 26】情境模拟:再好看也没用 !? 设计稿被工程师说太难做不出来 QQ

接下来,会就六角学院 UI 设计入门 课程中,针对团队合作时会碰到的情境稍作讨论。 设计稿再好看也没...