Kotlin 协程(一)

手机游戏开发者 2024-9-18 03:28:42 94 0 来自 中国
Come and Meet Kotlin Coroutine

Tags of Kotlin Coroutine

Kotlin协程可以被明白为一种轻量级的线程,它具有挂起规复的特点,可以将我们从异步编程的回调陷阱中解放出来
下面我们一一来看给协程贴上的标签怎样明白:

  • 挂起和规复

    • 挂起函数(suspend function)
    协程最吸引人的特点就在协程的挂起和规复特性上,通过这个特性我们可以或许像编写同步代码一样简化异步回调。这种特性在Kotlin语言层面表现为suspend关键字:
    // suspend functionsuspend fun function1() {    delay(1000L)    println("suspend function1")}// normal functionfun function2() {//    delay(2000L) not satisfy structural concurrency    println("suspend function2")}// type check:val funcVal1: suspend () -> Unit = ::function1val funcVal2: () -> Unit = ::function2相比平凡的函数,suspend函数可以明白为一种新的函数范例。

    • 协程构建器(Coroutine Builder)
    launch async runBlocking是三种常见的协程构建器,我们从函数署名上【感性】地熟悉一下他们的区别:
    // launchpublic fun CoroutineScope.launch(    context: CoroutineContext = EmptyCoroutineContext,    start: CoroutineStart = CoroutineStart.DEFAULT,    block: suspend CoroutineScope.() -> Unit): Job// aysncpublic fun <T> CoroutineScope.async(    context: CoroutineContext = EmptyCoroutineContext,    start: CoroutineStart = CoroutineStart.DEFAULT,    block: suspend CoroutineScope.() -> T): Deferred<T> // runBlockingpublic fun <T> runBlocking(    context: CoroutineContext = EmptyCoroutineContext,     block: suspend CoroutineScope.() -> T): T
我们通常可以利用launch启动一个协程,他的返回值Job可以用于控制这个协程的生命周期, async可以看做是一个升级版的launch,他的block的返回值会被放在Deferred中。Deferred是Job的子类,可以通过await方法获取返回值:
fun main() = runBlocking {    val job = launch {        println("execute of job")        "execute of job" // launch中的block不思量返回值,lambda的返回值会被忽略    }    val deferred = async {        println("execute of deferred")        "result of deferred"    }    println(deferred.await())    Unit}/**    execute of job    execute of deferred    result of deferred**/launch和async默认都是写了之后立刻启动(这一点非常紧张,aysnc并不必要await触发实验),可以通过调解CoroutineStart参数变更启动方式:
fun main() = runBlocking {    val lazyJob = launch(start = CoroutineStart.LAZY) {        println("execute now!")    }    println("before lazy starts")    // 通过delay先让父协程挂起,显着去别处launch没有立刻实验    println("parent sleeps")    delay(1000L)    println("parent wakes up")    lazyJob.start()    Unit}/**    before lazy starts    parent sleeps    parent wakes up    execute now!**/

  • 明白挂起和规复
下面我分别在两个suspend函数和两个由launch发起的协程中delay两秒,叨教main函数实验完因素别必要几秒?

  • suspend函数
fun main() = runBlocking {    getUserInfo()    getFriendList()}suspend fun getUserInfo() {    println("getUserInfo: start time: ${System.currentTimeMillis()}")    delay(2000L)    println("getUserInfo: end time: ${System.currentTimeMillis()}")    logX("suspend function1")}suspend fun getFriendList() {    println("getFriendList: start time: ${System.currentTimeMillis()}")    delay(2000L)    println("getFriendList end time: ${System.currentTimeMillis()}")    logX("suspend function2")}

  • Launch
fun main() = runBlocking {   launch {       println("launch1: start time: ${System.currentTimeMillis()}")       delay(2000L)       println("launch1: end time: ${System.currentTimeMillis()}")       logX("launch1")   }    launch {        println("launch2: start time: ${System.currentTimeMillis()}")        delay(2000L)        println("launch2: end time: ${System.currentTimeMillis()}")        logX("launch2")    }    Unit}答案发表时间:
suspend函数必要4秒,launch必要2秒。我们来看看挂起函数和launch的实验模子:
suspend函数和launch这类的协程构建器是有本质上的差别的,suspend函数在Kotlin编译器的作用下会变成一个主动机,而launch这类都不是suspend,他们实在是将任务【分发】到线程池(在JVM平台上)上实现的实验。
suspend和协程构建器的连合之处就在await上:
public suspend fun await(): Tawait是一个挂起函数,后续的流程会像上图以上被挂起,我们来看这个例子:
fun main() = runBlocking {    val def = async {        println("async starts")        delay(2000L)        println("async end")        "hello world"    }    println("message from main")    println(def.await())    println("end of story")}/**    message from main    async starts    async end    hello world // end of story的输出被挂起到await实验完成再规复    end of story**/suspend函数到主动机的转换在末了一节会阐明。Kotlin Coroutine狭义的协程指的是通过构建器启动的协程,后文不再阐明。

  • 轻量级的线程

    • 怎样明白【轻量级】
    在下面的代码中我们开启了许多个协程,但是等量的线程会OOM
    fun main() = runBlocking {    repeat(1000_000_000) {        launch { //常见的协程            delay(1000000)        }    }    delay(10000L)}

    • Kotlin Coroutine VS Thread

协程本身是运行在线程池上的:
fun main() = runBlocking {    logX("main ")    val job = launch(Dispatchers.IO) {        logX("launch 1")    }}/**================================main Thread:main @coroutine#1================================================================launch 1ThreadefaultDispatcher-worker-1 @coroutine#2================================**/ Dispatchers就可以指定运行的线程池。

2.gif Structured Concurrency

布局化并发的头脑贯穿于Kotlin coroutine的始终,我通过一句话概述:控制协程实验的范围。这个范围利用CoroutineScope实现。由于上面的代码都运行在runBlocking中,传入参数的时间直接将block设置为CoroutineScope的扩展lambda,以是不必要再指定scope:
// runBlockingpublic fun <T> runBlocking(    context: CoroutineContext = EmptyCoroutineContext,     block: suspend CoroutineScope.() -> T): T 包罗suspend函数也必要运行在scope中,否则就会在编译期报错。
Suspend Function : A CPS Transformation

Kotlin编译器会对挂起函数举行转换,如图所示:

3.gif 这种转换在Kotlin中被称为CPS(continuation-passing-style)转换,Continuation可以明白为是存储了中央过程的Callback。下面我们详细看一个例子:
要留意什么?

  • 编译后新增长的匿名内部类:TestContinuation
  • 看【挂起】和【规复】的逻辑:invokeSuspend
下面代码将编译前的挂起函数和编译后的挂起函数举行了一个比力,在编译后的testCoroutine中增长了一个新的匿名内部类,TestContinuation,此中纪录了获取的结果的信息,同时留意看invokeSuspend方法,这个方法有点像递归,末了还会调用到自身,但是会走差别的状态机的分支逻辑:
// 编译前的代码suspend fun testCoroutine() {    log("start")    val user = getUserInfo()    log(user)    val friendList = getFriendList(user)    log(friendList)    val feedList = getFeedList(friendList)    log(feedList)}// 编译后的代码fun testCoroutine(completion: Continuation<Any?>): Any? {    // TestContinuation本质上是匿名内部类    class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {        // 表示协程状态机当前的状态        var label: Int = 0        // 三个变量,对应原函数的三个变量        lateinit var user: String        lateinit var friendList: String        lateinit var feedList: String        // result 吸收协程的运行结果        var result = continuation.result        // suspendReturn 吸收挂起函数的返回值        var suspendReturn: Any? = null        // CoroutineSingletons 是个摆列类        // COROUTINE_SUSPENDED 代表当前函数被挂起了        val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED        // invokeSuspend 是协程的关键        // 它终极会调用 testCoroutine(this) 开启协程状态机        // 状态机干系代码就是反面的 when 语句        // 协程的本质,可以说就是 CPS + 状态机        override fun invokeSuspend(_result: Result<Any?>): Any? {            result = _result            label = label or Int.Companion.MIN_VALUE            return testCoroutine(this)        }    }    // ...    val continuation = if (completion is TestContinuation) {        completion    } else {        //                作为参数        //                   ↓        TestContinuation(completion)    }}testCoroutine运行的逻辑如下:
协程状态机的核心逻辑反编译后的伪代码如下:
when (continuation.label) {    0 -> {        // 检测非常        throwOnFailure(result)        log("start")        // 将 label 置为 1,预备进入下一次状态        continuation.label = 1        // 实验 getUserInfo        suspendReturn = getUserInfo(continuation)        // 判定是否挂起        if (suspendReturn == sFlag) {            return suspendReturn        } else {            result = suspendReturn            //go to next state        }    }    1 -> {        throwOnFailure(result)        // 获取 user 值        user = result as String        log(user)            // 预备进入下一个状态        continuation.label = 2        // 实验 getFriendList        suspendReturn = getFriendList(user, continuation)        // 判定是否挂起        if (suspendReturn == sFlag) {            return suspendReturn        } else {            result = suspendReturn            //go to next state        }    }    2 -> {        throwOnFailure(result)        user = continuation.mUser as String        // 获取 friendList 的值        friendList = result as String        log(friendList)        // 预备进入下一个状态        continuation.label = 3        // 实验 getFeedList        suspendReturn = getFeedList(user, friendList, continuation)        // 判定是否挂起        if (suspendReturn == sFlag) {            return suspendReturn        } else {            result = suspendReturn            //go to next state        }    }    3 -> {        throwOnFailure(result)        user = continuation.mUser as String        friendList = continuation.mFriendList as String        feedList = continuation.result as String        log(feedList)        loop = false    }}我们来捋一下此中的次序,最开始先构建一个TestContinuation的实例,留意,Continuation的这个实例是三个挂起函数的公共参数。

  • getUserInfo
开始时label = 0, 此时进入逻辑,先举行非常的查抄,设置下一次的入口label=1,实验getUserInfo:
when (continuation.label) {    0 -> {        // ...        continuation.label = 1        // 实验 getUserInfo        suspendReturn = getUserInfo(continuation)        // 判定是否挂起        if (suspendReturn == sFlag) {            return suspendReturn        } else {            result = suspendReturn            //go to next state        }    }    // ...}在Kotlin编译器CPS转换之后的getUserInfo方法中,由于传入了continuation参数,必要再实验一次Continuation#invokeSuspend,这个方法同时也将结果纪录在了result处
override fun invokeSuspend(_result: Result<Any?>): Any? {     result = _result     label = label or Int.Companion.MIN_VALUE     return testCoroutine(this) }相当于【递归】地实验一次如许的逻辑(个人以为这个逻辑和通报变乱的分发有点相似)。此时getUserInfo实验完成返回的结果是CoroutineSingletons.COROUTINE_SUSPEND,以是继承实验下个when的case。
反面的结果其他的挂起函数的实验过程都差不多。详细过程如图所示:
通过这个状态机的分析可以或许让我们更加深刻的明白挂起函数中【挂起】和【规复】的本质:实在就是基于状态机的回调函数,但是这种回调函数的实验逻辑是Kotlin编译器主动天生的,大大淘汰了我们的脑力斲丧。
必要留意的是,以上的挂起函数都是【真正的】挂起函数,suspend function中都带有挂起的操纵,但是Kotlin编译器在举行CPS转换的时间只认supsend关键字,对于伪suspend函数,走else分支,节流开销:
if (suspendReturn == sFlag) {      return suspendReturn } else {      result = suspendReturn      //go to next state }
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2025-2-6 14:05, Processed in 0.161161 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表