Originally written by Manuel Vivo

Modifier — Under The Hood

Translator: Jingpingcheng

This article will help you understand how the suspend method works.

Coroutines 101

Using Coroutines(later referred to as Coroutines) on Android will help simplify the development of asynchronous tasks. Using coroutines to manage asynchronous tasks avoids blocking the main thread.

With coroutines, we no longer need to write callback style code:

// Simplified code that only considers the happy path
fun loginUser(userId: String, password: String, userResult: Callback<User>) {
  // Async callbacks
  userRemoteDataSource.logUserIn { user ->
    // Successful network request
    userLocalDataSource.logUserIn(user) { userDb ->
      // Result saved in DB
      userResult.success(userDb)
    }
  }
}
Copy the code

The above code is implemented using coroutines:

suspend fun loginUser(userId: String, password: String): User {
  val user = userRemoteDataSource.logUserIn(userId, password)
  val userDb = userLocalDataSource.logUserIn(user)
  return userDb
}
Copy the code

We notice that the modified method adds the suspend modifier. When the compiler encounters the SUSPEND modifier, it prompts us that the method must be called inside one coroutine or another suspend method. The suspend method differs from normal methods in that it has the ability to suspend and resume.

Unlike callbacks, coroutines give us an easy way to cut threads and handle exceptions.

So what does the compiler do for us?

Suspend under the hood

Back to the loginUser method, other time-consuming operations in this method are also defined as the suspend method:

suspend fun loginUser(userId: String, password: String): User {
  val user = userRemoteDataSource.logUserIn(userId, password)
  val userDb = userLocalDataSource.logUserIn(user)
  return userDb
}

// UserRemoteDataSource.kt
suspend fun logUserIn(userId: String, password: String): User

// UserLocalDataSource.kt
suspend fun logUserIn(userId: String): UserDb
Copy the code

The Kotlin compiler converts the suspend method into a callback method based on a finite state machine implementation.

Yes, coroutines are still based on callbacks in essence, but the compiler writes the callback code for you.

Continuation interface

The Continuation interface is used to collaborate between suspend methods. The Continuation interface is a generic callback interface that contains some additional information.

Let’s look at the Continuation interface definition:

interface Continuation<in T> {
  public val context: CoroutineContext
  public fun resumeWith(value: Result<T>)
}
Copy the code
  • parametercontext, the context of the coroutineCoroutineContext.
  • resumeWithA callback method called after the logic inside the coroutine has finished executing.ResultA collaborators routine returns of composite object on Success or Failure, namely Success T | Failure Throwable.

Note: Starting with Kotlin version 1.3, you can also use the extension methods resume(value: T) and resumeWithException(exception: Throwable) for new continuations.

public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))
    
public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
    resumeWith(Result.failure(exception))
Copy the code

The Kotlin compiler replaces the suspend keyword with a Continuation type parameter completion in the method signature, which is used to pass execution results to the coroutine calling it.

fun loginUser(userId: String, password: String, completion: Continuation<Any? >) {
  val user = userRemoteDataSource.logUserIn(userId, password)
  val userDb = userLocalDataSource.logUserIn(user)
  completion.resume(userDb)
}
Copy the code

To simplify the example, the return value of the loginUser method here is Unit instead of User. The User will be returned by calling the Resume method on Completion.

In fact, the suspend method returns a result of type Any? , it is a complex type, namely T | COROUTINE_SUSPENDED.

Note: If no other suspend methods are called inside a suspend method, the compiler still adds a Continuation parameter to the method signature, but does not use it, just like a normal method.

The Continuation interface can also be used in the following scenarios:

  • When usingsuspendCoroutineorsuspendCancellableCoroutineWhen converting callback – based APIs, you can use it directlyContinuationObject to recover a coroutine that was suspended because it was executing blocking code.

Translator’s note: To add an example

suspend fun requestDataSuspend(a) = suspendCoroutine { continuation ->
    requestDataFromServer { data -> // The normal method is to receive data via callback
        if (data! =null) {
            continuation.resume(data)}else {
            continuation.resumeWithException(MyException())
        }
    }
}
Copy the code
  • You can use the extension methodstartCoroutineTo start a coroutine. The method receives oneContinuationObject that returns a result or exception when the coroutine completes executionContinuationObject’s callback method is called.

Translator’s note: To add an example

val suspendLambda = suspend {
    "Hello world!"
}
val completion = object : Continuation<String> {
    override val context get() = EmptyCoroutineContext
    override fun resumeWith(result: Result<String>) {
        println(result.getOrThrow())The return value of suspendLambda was received
        println("completion is called")
    }
}
suspendLambda.startCoroutine(completion)

// print
Hello world!
completion is called
Copy the code

Using different Dispatchers

You can use Dispatchers to cut threads. How does Kotlin know which thread to resume the suspend operation on? A subclass of Continuations, DispatchedContinuation, calls the Dispatcher dispatch method available in CoroutineContext in its resumeWith method. All Dispatchers call the Dispatch method except dispatchers. Unconfined because its isDispatchNeeded method always returns false.

The generated State machine

Declaration: The sample code in the following article is not exactly equivalent to the compiler-generated bytecode, but it is accurate enough to help us understand how the suspend method works. The sample code is based on version 1.3.3 of Coroutines.

The Kotlin compiler marks each suspend point as a state inside the suspend method, based on a finite state machine. These states can be expressed as labels:

fun loginUser(userId: String, password: String, completion: Continuation<Any? >) {
  
  // Label 0 -> first execution
  val user = userRemoteDataSource.logUserIn(userId, password)
  
  // Label 1 -> resumes from userRemoteDataSource
  val userDb = userLocalDataSource.logUserIn(user)
  
  // Label 2 -> resumes from userLocalDataSource
  completion.resume(userDb)
}
Copy the code

To better represent the state machine, the compiler uses when to distinguish between different states:

fun loginUser(userId: String, password: String, completion: Continuation<Any? >) {
  when(label) {
    0- > {// Label 0 -> first execution
        userRemoteDataSource.logUserIn(userId, password)
    }
    1- > {// Label 1 -> resumes from userRemoteDataSource
        userLocalDataSource.logUserIn(user)
    }
    2- > {// Label 2 -> resumes from userLocalDataSource
        completion.resume(userDb)
    }
    else -> throw IllegalStateException(/ *... * /)}}Copy the code

The code is incomplete because there is no way to share information between different states. The compiler uses a Continuation object in the method signature to share information. That’s why the type of Continuation is Any? The reason why.

In addition, the compiler creates a private class

  1. Store shared data
  2. Recursive callsloginUserMethod to resume execution.
fun loginUser(userId: String? , password:String? , completion:Continuation<Any? >) {
  
  class LoginUserStateMachine(
    // The completion parameter recursively invokes the loginUser method as a callbackcompletion: Continuation<Any? > ): CoroutineImpl(completion) {// Save the result returned by the suspend method
    var user: User? = null
    var userDb: UserDb? = null
  
    // Save the state machine execution data
    var result: Any? = null
    var label: Int = 0
  
    // This method calls the loginUser method again, triggering the next execution of the state machine
    // Result is the computed result of the previous state, and label is set to the next state value
    override fun invokeSuspend(result: Any?). {
      this.result = result
      loginUser(null.null.this)}}/ *... * /
}
Copy the code

The invokeSuspend method calls the loginUser method again, with the userId and password arguments empty, and the Continuation object carrying the information needed for the state machine to resume execution.

The state machine must know:

  1. Whether the method is executed for the first time
  2. Whether the method resumes execution after the last suspension
fun loginUser(userId: String? , password:String? , completion:Continuation<Any? >) {
  / *... * /
  val continuation = completion as? LoginUserStateMachine ? : LoginUserStateMachine(completion)/ *... * /
}
Copy the code

If the method is executed for the first time, a new LoginUserStateMachine instance is created and a Completion instance is stored to hold information about the Continuation when it is suspended. If it is not the first time, the next step in the state machine is executed.

fun loginUser(userId: String? , password:String? , completion:Continuation<Any? >) {
    / *... * /

    val continuation = completion as? LoginUserStateMachine ? : LoginUserStateMachine(completion)when(continuation.label) {
        0- > {// Check failed
            throwOnFailure(continuation.result)
            // The status flag is set to 1 in preparation for the next state operation
            continuation.label = 1
            / / convey a continuation to userRemoteDataSource logUserIn method
            // After the logUserIn method is executed, the next operation of the execution state machine is performeduserRemoteDataSource.logUserIn(userId!! , password!! , continuation) }1- > {// Check failed
            throwOnFailure(continuation.result)
            // Get the execution result of the previous operation from the continuation
            continuation.user = continuation.result as User
            // The status flag is at position 2, ready for the next state operation
            continuation.label = 2
            / / convey a continuation to userLocalDataSource logUserIn method
            // After the logUserIn method is executed, the next operation of the execution state machine is performed
            userLocalDataSource.logUserIn(continuation.user, continuation)
        }
          /* ... leaving out the last state on purpose */}}Copy the code

Let’s take a look at this code:

  • whenConditionally checkedlabelThe parameter comes fromLoginUserStateMachineInstance.
  • Each time a new state is executed, check to see if the last pending method call failed.
  • Before executing the next suspend method, place the flag bitlabelUpdate the status value to perform the next operation.
  • During state machine execution, if another suspend method is called, the currentcontinuationThe instance is passed to it as an argument to the method.

The invoked suspend method also has its own state machine that receives the continuation instance and calls back the Continuation instance’s Resume method when it completes execution. After a continuation instance calls the resume method, the LoginUserStateMachine instance’s invokeSuspend method is internally called to resume the next state operation. For details, see the resumeWith method at ContinuationImp.kt.

The final step of the state machine execution is different. It calls the resume method and returns the result of the execution as an argument to the method caller.

fun loginUser(userId: String? , password:String? , completion:Continuation<Any? >) {
    / *... * /

    val continuation = completion as? LoginUserStateMachine ? : LoginUserStateMachine(completion)when(continuation.label) {
        / *... * /
        2- > {// Check failed
            throwOnFailure(continuation.result)
            // Get the result from the previous state operation
            continuation.userDb = continuation.result as UserDb
            // Resume execution from the method call
            continuation.cont.resume(continuation.userDb)
        }
        else -> throw IllegalStateException(/ *... * /)}}Copy the code

As you can see, the Kotlin compiler does all this for us! From hanging methods like this:

suspend fun loginUser(userId: String, password: String): User {
  val user = userRemoteDataSource.logUserIn(userId, password)
  val userDb = userLocalDataSource.logUserIn(user)
  return userDb
}
Copy the code

The compiler generates the following code for us:

fun loginUser(userId: String? , password:String? , completion:Continuation<Any? >) {

    class LoginUserStateMachine(
        // The completion parameter recursively invokes the loginUser method as a callbackcompletion: Continuation<Any? > ): CoroutineImpl(completion) {// Save the result returned by the suspend method
        var user: User? = null
        var userDb: UserDb? = null

        // Save the state machine execution data
        var result: Any? = null
        var label: Int = 0

        // This method calls the loginUser method again, triggering the next execution of the state machine
        // Result is the computed result of the previous state, and label is set to the next state value
        override fun invokeSuspend(result: Any?). {
            this.result = result
            loginUser(null.null.this)}}val continuation = completion as? LoginUserStateMachine ? : LoginUserStateMachine(completion)when(continuation.label) {
        0- > {// Check failed
            throwOnFailure(continuation.result)
            // The status flag is set to 1 in preparation for the next state operation
            continuation.label = 1
            / / convey a continuation to userRemoteDataSource logUserIn method
            // After the logUserIn method is executed, the next operation of the execution state machine is performeduserRemoteDataSource.logUserIn(userId!! , password!! , continuation) }1- > {// Check failed
            throwOnFailure(continuation.result)
            // Get the result from the previous state operation
            continuation.user = continuation.result as User
            // The status flag is at position 2, ready for the next state operation
            continuation.label = 2
            / / convey a continuation to userLocalDataSource logUserIn method
            // After the logUserIn method is executed, the next operation of the execution state machine is performed
            userLocalDataSource.logUserIn(continuation.user, continuation)
        }
        2- > {// Check failed
            throwOnFailure(continuation.result)
            // Get the result from the previous state operation
            continuation.userDb = continuation.result as UserDb
            // Resume execution from the method call
            continuation.cont.resume(continuation.userDb)
        }
        else -> throw IllegalStateException(/ *... * /)}}Copy the code

Resume (continuation. UserDb) does not call the loginUser method again, but returns the result of the entire operation to the caller.

Summary: The Kotlin compiler converts each suspended method into a state machine, and each suspended method is eventually restored through a callback.

In this article, we can better understand why a suspended method does not return an execution result immediately but hangs until it completes, and why a suspended method does not block a thread. The magic of all this is in the Continuation object, which carries the contextual information needed for the method to resume execution.