天天看点

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

作者:不秃头程序员
Kotlin中的挂起函数如何工作的

挂起能力是构建所有其他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<Unit> { }
  println("After")
}
// 打印 "Before"           

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

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

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

suspend fun main() {
  println("Before")
  suspendCoroutine<Unit> { 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<*>): Any
fun 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 hood
fun myFunction(continuation: Continuation<Unit>): 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<Unit>): 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 working
class MyFunctionContinuation(val completion: Continuation<Unit>) : Continuation<Unit> {
  override val context: CoroutineContext
    get() = completion.context
  
  var result: Result<Unit>? = null
  var label = 0
  var counter = 0 //save like a state

  override fun resumeWith(result: Result<Unit>) {
    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 example

var result: Result<Any>? = 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 = ...           

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

  • 挂起函数就像状态机,在函数开始处和每次挂起函数调用之后都有一个可能的状态。
  • 标识状态的标签和本地数据都保存在延续对象中。
  • 一个函数的继续可以装饰其调用者函数的继续;因此,所有这些继续表示当我们恢复或恢复的函数完成时使用的调用栈。

继续阅读