laitimes

How the suspend function works in Kotlin

author:Not bald programmer
How the suspend function works in Kotlin

The ability to suspend is the most fundamental feature on which all other Kotlin coroutine concepts are built.

In short: suspending a coroutine means that it can be stopped halfway through.

Analogy: Start by understanding the true meaning of suspension through some real-world examples:

Play Video Games:

  • You're playing well (assuming).
  • Arrive at a checkpoint.
  • Save your current location.
  • Turn off the game and both you and your PC are now focused on doing different things.
  • Complete the quest and come back and pick up where you left off.

Imagine you're multitasking in the kitchen:

  • Start a recipe (coroutine initiation): Start a recipe by chopping vegetables (coroutine task 1).
  • Pause and switch tasks (coroutine suspended): The oven needs to be warmed up (coroutine task 2). Pause chopping vegetables (suspend task 1) and set up the oven (complete task 2).
  • Continue to the first task (coroutine recovery): Once the oven is preheated or turned on, go back to where you cut vegetables (resume task 1) and continue.
  • Repeat as needed (multiple coroutines): You can switch tasks (coroutines) while waiting for something else, such as checking if water is boiling (another coroutine task).

Now, these examples are the best analogy for coroutine hangs. A coroutine can start executing a function, it can suspend (save state and set aside threads for others), and then resume once any suspended tasks (such as network calls) have been executed.

Let's take a look at a suspend function in action:

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

It is a simple program that will print "Before" and "After". What happens if we suspend between these two prints? To do this, you can use the suspendCoroutine function provided by the standard Kotlin library.

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

If you call the above code, you won't see "After" and the code won't stop running (because the main function never completes). Coroutines are suspended after "Before". Our game was stopped and not resumed. So, how do you recover?

This suspendCoroutine call ends with a lambda expression ({ }). The function passed as a parameter will be called before it is suspended. This function gets a continuation as an argument.

This continuation can be used to recover our coroutines. This lambda is used to store this continuation somewhere or to plan whether to recover it or not. You can use it to recover instantly:

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

As you might imagine, here we suspend and resume immediately. It's a good gut feeling, but the truth is that there is an optimization that prevents not hanging if it resumes immediately.

Use the "delay" function to suspend a coroutine for a specific amount of time and then resume it.

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

It's important to emphasize that we're suspending coroutines, not functions. A suspend function is not a coroutine, just a function that can suspend a coroutine.

Note that this is very different from threads, which cannot be saved, only blocked.

Coroutines are much more powerful. When suspended,

  • It does not consume any resources.
  • Coroutines can be recovered on different threads
  • Continuation can be serialized, deserialized, and then restored.

Internal Principle:

In Kotlin, the suspend function is implemented using the Confirmation transitive style. This means that continuations are passed as arguments from function to function (like Composer in Jetpack Compose).

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           

The return type is changed to Any because the suspend function can return a "COROUTINE_SUSPENDED" in addition to the defined return type.

Let's take a look at a simple suspend function:

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

Next, this function needs its continuation to remember its state. Let's call it "MyFunctionContinuation".

Functions can start in two places: either from the start (if it's the first call) or after the hang start (if it's from continuation). To identify the current state, a field called label is used. At the beginning, it is 0, so the function will start from the beginning. However, before each hanging point, it is set to the next state, so that after the recovery we start after the hanging point.

// 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")
}           

When a deferred call is made, COROUTINE_SUSPENDED is returned, and then myFunction returns COROUTINE_SUSPENDED; the function that called it, the function that called it, and all other functions up to the top of the call stack do the same. This is how suspending ends all of these functions and makes the thread available to other runnable objects, including coroutines.

What happens if this "deferred" call doesn't return COROUTINE_SUSPENDED? What if it just returns units? Note that if the deferred call returns only one unit, we'll move to the next state, and the function will behave like any other state.

And, the inner continuation class looks like this

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

If you want to store state:

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

Here the counter needs to be used in both states (for labels equal to 0 and 1), so it needs to be kept in the continuation. It will be stored before it is suspended. Restoring these types of properties occurs at the beginning of the function. This is the internal structure of the function:

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)
  }
}           

For the most common use cases (e.g. making API calls).

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")
}           

The difference in this case is that

  • Store the results of the function as follows......
...
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
...           
  • Restore the state at the beginning of the function, just like we did with the "counter" variable:
var counter = continuation.counter// In previous example

var result: Result<Any>? = continuation.result// In this example(when suspend function returns a value)           

One thing to note here is that Continuations will act as our call stack and store the state of the function (labels, arguments, variables).

If A and B are two suspend functions, and A calls B in them. B then stores A's Contribution as its Completion state.

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

Internally, these things are implemented in a way that looks more complex in order to optimize. This is done in a loop rather than recursively.

  • A suspend function is like a state machine, with a possible state at the beginning of the function and after each suspend function call.
  • Both the label that identifies the state and the local data are saved in a continuation object.
  • The continuation of a function can decorate the continuation of its caller function; so all of these continuation represent the call stack that is used when we resume or resume the function completes.
Job

Read on