Kotlin中的挂起函数如何工作的

程序员咋不秃头2024-04-17 21:38:00  66

挂起能力是构建所有其他Kotlin协程概念的最基本特性。

简而言之:挂起协程意味着可以在中途停止它。

类比: 首先通过一些现实世界中的例子来理解挂起的真正含义:

玩视频游戏:

你玩得很好(假设)。

到达一个检查点。

保存当前的位置。

关掉游戏,你和你的电脑现在都专注于做不同的事情。

完成任务回来并从保存的地方继续。

想象一下你在厨房中多任务处理:

开始一个食谱(协程启动):通过切菜开始一个食谱(协程任务1)。

暂停并切换任务(协程挂起):烤箱需要预热(协程任务2)。暂停切菜(挂起任务1)并设置烤箱(完成任务2)。

继续第一个任务(协程恢复):一旦烤箱预热或打开,回到切菜(恢复任务1)的地方继续。

根据需要重复(多个协程):你可以在等待其他事情的同时切换任务(协程),比如检查水是否沸腾(另一个协程任务)。

现在,这些示例是协程挂起最好的类比。 一个协程可以开始执行函数,它可以挂起(保存状态并为他人留出线程),然后一旦任何挂起任务(如网络调用)执行完毕后恢复。

看一个挂起函数的实际操作:

suspend fun main { println("Before") println("After")}// 打印 "Before"// 打印 "After"

这是一个简单的程序,将打印“Before”和“After”。如果我们在这两个打印之间挂起会发生什么呢?为此,可以使用标准Kotlin库提供的suspendCoroutine函数。

suspend fun main { println("Before") suspendCoroutine { } println("After")}// 打印 "Before"

如果调用上述代码,将不会看到“After”,并且代码不会停止运行(因为main函数从未完成)。协程在“Before”之后被挂起。我们的游戏被停止且未恢复。那么,如何恢复呢?

这个suspendCoroutine调用以一个lambda表达式({ })结束。作为参数传递的函数将在挂起之前被调用。这个函数获得一个continuation作为参数。

可以使用这个continuation来恢复我们的协程。这个lambda用于将这个continuation存储在某处或计划是否恢复它。可以用它来立即恢复:

suspend fun main { println("Before") suspendCoroutine { continuation -> continuation.resume(Unit) } println("After")}// 打印 "Before"// 打印 "After"

你可能会想到,这里我们挂起并立即恢复。这是一个好的直觉,但事实是,存在一种优化,可以防止如果立即恢复则不挂起。

使用“delay”函数来挂起协程特定的时间然后恢复它。

suspend fun main { println("Before") delay(1000) println("After")}// 打印 "Before"// (挂起1秒)// 打印 "After"

需要强调的一点是,我们挂起的是协程,而不是函数。挂起函数不是协程,只是可以挂起协程的函数。

注意,这与线程非常不同,线程不能被保存,只能被阻塞。

协程要强大得多。当挂起时,

它不消耗任何资源。

协程可以在不同的线程上恢复

continuation可以被序列化,反序列化,然后恢复。

内部原理:

在Kotlin中,挂起函数是使用Continuation传递风格实现的。这意味着continuations作为参数从函数传递到函数(就像Jetpack Compose中的Composer)。

suspend fun getUser: User?suspend fun setUser(user: User)suspend fun checkAvailability(flight: Flight): Boolean// 内部看起来像fun getUser(continuation: Continuation<*>): Any?fun setUser(user: User, continuation: Continuation<*>): Anyfun checkAvailability(flight: Flight,continuation: Continuation<*>): Any

返回类型变更为Any,因为除了定义的返回类型之外,挂起函数还可以返回一个“COROUTINE_SUSPENDED”。

看一个简单的挂起函数:

suspend fun myFunction { println("Before") delay(1000) // 挂起 println("After")}

接下来,这个函数需要它的continuation来记住它的状态。让我们称它为“MyFunctionContinuation”。

函数可以从两个地方开始:要么从开始(如果是首次调用),要么从挂起点之后(如果是从continuation恢复)。为了识别当前状态,使用了一个称为label的字段。在开始时,它是0,因此函数将从开始处开始。然而,在每个挂起点之前,它被设置为下一个状态,以便在恢复后我们从挂起点之后开始。

// A simplified picture of how myFunction looks under the hoodfun myFunction(continuation: Continuation): Any { if (continuation.label == 0) { //Starting point println("Before") continuation.label = 1 //Update just before suspension if (delay(1000, continuation) == COROUTINE_SUSPENDED){ return COROUTINE_SUSPENDED } } //Point after suspension if (continuation.label == 1) { println("After") return Unit } error("Impossible")}

当延迟调用时,返回COROUTINE_SUSPENDED,然后myFunction返回COROUTINE_SUSPENDED;调用它的函数、调用该函数的函数以及直到调用堆栈顶部的所有其他函数都会执行相同的操作。这就是挂起结束所有这些函数并使线程可供其他可运行对象(包括协程)使用的方式。

如果这个“延迟”调用没有返回 COROUTINE_SUSPENDED 会发生什么?如果它只是返回单位怎么办?请注意,如果延迟仅返回一个 Unit,我们将移至下一个状态,并且该函数的行为将与其他状态一样。

并且,内部延续类看起来像这样

cont = object : ContinuationImpl(continuation) { var result: Any? = null var label = 0 override fun invokeSuspend(`$result`: Any?): Any? { this.result = `$result`; return myFunction(this); }};

如果你想存储状态:

suspend fun myFunction { println("Before") var counter = 0 //local state delay(1000) // suspending counter++ println("Counter: $counter") println("After")}

这里需要在两种状态下使用计数器(对于等于0和1的标签),因此需要将其保留在延续中。它将在暂停之前存储。恢复这些类型的属性发生在函数的开头。这就是函数的内部结构:

fun myFunction(continuation: Continuation): Any { var counter = continuation.counter //restoring the value at start if (continuation.label == 0) { println("Before") counter = 0 //user-defined continuation.counter = counter //saving the value just before suspension continuation.label = 1 if (delay(1000, continuation) == COROUTINE_SUSPENDED){ return COROUTINE_SUSPENDED } } if (continuation.label == 1) { counter = (counter as Int) + 1 //user-defined println("Counter: $counter") println("After") return Unit } error("Impossible")}//Continuation object internal workingclass MyFunctionContinuation(val completion: Continuation) : Continuation { override val context: CoroutineContext get = completion.context var result: Result? = null var label = 0 var counter = 0 //save like a state override fun resumeWith(result: Result) { this.result = result val res = try { val r = myFunction(this) if (r == COROUTINE_SUSPENDED) return Result.success(r as Unit) } catch (e: Throwable) { Result.failure(e) } completion.resumeWith(res) }}

对于最常见的用例(例如进行 API 调用).

suspend fun printUser(token: String) { println("Before") val userId = getUserId(token) // suspending network call println("Got userId: $userId") val userName = getUserName(userId, token) // suspending network call println(User(userId, userName)) println("After")}

在这种情况下的不同之处在于,

存储函数的结果,如下所示......

...val res = getUserId(token, continuation)if (res == COROUTINE_SUSPENDED) {return COROUTINE_SUSPENDED}result = Result.success(res)//store the success when when function didn't suspend...

恢复函数开始时的状态,就像我们对“counter”变量所做的那样:

var counter = continuation.counter// In previous examplevar result: Result? = continuation.result// In this example(when suspend function returns a value)

这里需要注意的一件事是,Continuations 将充当我们的调用堆栈并存储函数的状态(标签、参数、变量)。

如果 A 和 B 是两个挂起函数,并且 A 在其中调用 B。然后B也会将A的Continuation存储为它的Completion状态。

BContinuation( i = 4, label = 1, completion = AContinuation( i = 4, label = 1, completion = ...

在内部,为了优化以看起来更复杂的方式实现这些东西。用循环而不是递归来实现的。

挂起函数就像状态机,在函数开始处和每次挂起函数调用之后都有一个可能的状态。

标识状态的标签和本地数据都保存在延续对象中。

一个函数的继续可以装饰其调用者函数的继续;因此,所有这些继续表示当我们恢复或恢复的函数完成时使用的调用栈。

转载此文是出于传递更多信息目的。若来源标注错误或侵犯了您的合法权益,请与本站联系,我们将及时更正、删除、谢谢。
https://www.414w.com/read/283190.html
0
随机主题
被问是否会遵守ICC对内塔尼亚胡等人的裁决,德国政府发言人:当然山东首富杀入新能源汽车领域, 魏桥集团誓要打造山东汽车品牌12胜0负! 3连冠! 辽篮王朝!IGN评选最佳25款Switch游戏: 王泪第一野炊第二美国露怯? 拜登对华加征新关税后, 耶伦却喊话希望不要受到报复多亏他们! 大桥被冲垮前61人安全转移降价就会冲的小米手机, 很多人直接选了这两款, 小米13Ultra在内骁龙8Gen2旗舰直降400,金属边框+索尼大底,体验不输一加12东方财富网股吧社区将开展内容专项治理江苏调度100万台农机具助力“三夏”生产冠军信赖真靠谱 捷途X70 PLUS冠军版正式上市《斗破》三千雷动对萧炎多重要? 堪比天阶斗技, 被风雷阁设阵追杀如果北约跟俄罗斯开火了,中国怎么办韩服又一顶尖玩法,连出6把锯齿短匕,每一刀都是真实伤害!比亚迪皮卡详细参数曝光, 百公里油耗仅7.5L, 后排空间亮眼小米SU7上市之后,新能源二手车绷不住了,特斯拉也拉裤兜子了因涉嫌信披违法违规 超卓航科及董事长李光平等被警告及罚款拜仁新帅只差官宣!38岁降级队教练登陆豪门,5大豪门拒绝穆里尼奥新趋势!北青:伊万征调多名跑动能力和身体条件俱佳的国脚!《夜族崛起》暗黑来袭——是德古拉也是伐木工你的第一把全铝客制化键盘 只需要三百多的玄派PD75mV2首发套件
最新回复(0)