The Kotlin compiler generates a state machine for each suspend function to manage the execution of the coroutine.
Coroutines simplifies asynchronous operations on Android. As explained in the documentation, we can use them to manage asynchronous tasks that might otherwise block the main thread, causing your application to Crash.
Coroutines also helps replace callback-based apis with imperative code.
As an example, let’s first look at asynchronous code that uses callbacks.
// 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
These callbacks can be converted into sequential function calls 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
In the Coroutines code, we add the suspend modifier to the function. This tells the compiler that the function needs to be executed within a Coroutine. As a developer, you can think of the suspend function as a normal function, but its execution may be suspended and resumed at some point.
Suspend, in short, is a compiler-generated callback.
Unlike callbacks, Coroutines provide a simple way to switch between threads and handle exceptions.
But what is the compiler actually doing behind the scenes when we mark our function suspend?
What exactly does Suspend do
Back to the suspend function of loginUser, notice that the other functions it calls are also suspend functions.
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
In short, the Kotlin compiler uses a finite state machine (which we’ll cover later) to convert the suspend function into an optimized version of the callback implementation. You’re right, the compiler will write these callbacks for you, and they’re still callbacks by nature!
The true face of Continuation
The suspend functions communicate with each other using Continuation objects. A Continuation is just a generic callback interface with some extra information. As we will see later, it will represent a state machine for the suspend function.
Let’s look at the definition.
interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(value: Result<T>)
}
Copy the code
- Context is the CoroutineContext used in continuations.
- ResumeWith resumes the execution of a Coroutine with a Result, which can contain a value that led to the suspend calculation or an exception.
Note: Starting with Kotlin 1.3, you can also use the extension functions Resume (value: T) and resumeWithException(Exception: Throwable), which are special versions of the resumeWith call.
The compiler replaces the suspend decorator with an additional parameter in the function signature, completion (of the Continuation type), which is used to convey the results of the suspend function 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
For simplicity, our example will return Unit instead of User. The User object will be “returned” in the added Continuation parameter.
The bytecode of the suspend function actually returns Any? Because it is (T | COROUTINE_SUSPENDED) joint types. This allows the function to return synchronously when it can.
Note: If you mark a function with the suspend modifier that doesn’t call any other suspend functions, the compiler adds an extra Continuation argument as well, but doesn’t do anything with it, and the bytecode of the function body looks like an ordinary function.
You can also see the Continuation interface elsewhere.
- When using suspendCoroutine or suspendCancellableCoroutine converts API based on callback to a coroutine when (you should have a tendency to use this method), you directly with a Continuation object interaction, To resume a code block that was passed as a parameter to suspend at runtime.
- You can start a coroutine using the startCoroutine extension function on the suspend function. It takes a Continuation object as an argument, and when the new Coroutine completes, both the result and the exception will be called.
Switch between different Dispatchers
You can swap between different Dispatchers and perform calculations on different threads. So how does Kotlin know where to resume a suspended calculation?
Continuations have a subtype, called DispatchedContinuation, whose Resume function makes scheduling calls to dispatchers available in CoroutineContext. Dispatch is called by all Dispatchers except the dispatchers. Unconfined isDispatchNeeded function override (called before Dispatch), which always returns false.
There is an unwritten convention in coroutines that the suspend function does not block by default. That is, callers do not have to worry about the thread on which the suspend function runs. The suspend function processes the thread on which it works. It’s all done with a withContext.
State machine generation
Disclaimer: The code shown in the rest of this article will not fully conform to the bytecode generated by the compiler. It will be Kotlin code accurate enough to allow you to understand what’s really going on inside. This notation was generated by Coroutines version 1.3.3 and may change in future versions of the library.
The Kotlin compiler recognizes when a function can suspend internally. Each suspend point is represented as a state in a finite state machine. These states are represented by the compiler with labels, and the suspend function in the previous example, when compiled, produces pseudocode similar to the one below.
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 a WHEN statement to implement 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 method by which the compiler compilers a suspend function with a Continuation argument is called the CPS (continuation-passing Style) transformation.
This code is incomplete because there is no way for different states to share information. The compiler uses the same Continuation object in the function to do this. That’s why the generics for continuations are Any? Not the return type of the original function (that is, User).
In addition, the compiler creates a private class that 1) holds the required data, and 2) recursively calls the loginUser function to resume execution. You can see an approximation of the generated class below.
Disclaimer: Comments are not generated by the compiler. I added them to explain what they do and to make the code that follows easier to understand.
fun loginUser(userId: String? , password: String? , completion: Continuation<Any? >) { class LoginUserStateMachine( // completion parameter is the callback to the function // that called loginUser completion: Continuation<Any? > ): CoroutineImpl(completion) { // Local variables of the suspend function var user: User? = null var userDb: UserDb? = null // Common objects for all CoroutineImpls var result: Any? = null var label: Int = 0 // this function calls the loginUser again to trigger the // state machine (label will be already in the next state) and // result will be the result of the previous state's computation override fun invokeSuspend(result: Any?) { this.result = result loginUser(null, null, this) } } ... }Copy the code
Because invokeSuspend will call loginUser again with only information from the Continuation object, the remaining parameters in the loginUser function signature become null. At this point, the compiler just needs to add information about how to transition between states.
The first thing it needs to do is know that 1) this is the first time the function has been called, or 2) the function has been restored from its previous state. This is done by checking whether the continuation passed in is of type LoginUserStateMachine.
fun loginUser(userId: String? , password: String? , completion: Continuation<Any? >) {... val continuation = completion as? LoginUserStateMachine ? : LoginUserStateMachine(completion) ... }Copy the code
If it’s the first time, it creates a new instance of LoginUserStateMachine and stores the completed instance it received as a parameter so it can remember how to restore the function that called it. If not, it simply continues to execute the state machine (suspend function).
Now let’s look at the code generated by the compiler to move and share information between states.
/* Copyright 2019 Google LLC.spdx-license-Identifier: Apache-2.0 */ fun loginUser(userId: String? , password: String? , completion: Continuation<Any? >) {... val continuation = completion as? LoginUserStateMachine ? : LoginUserStateMachine(completion) when(continuation.label) { 0 -> { // Checks for failures throwOnFailure(continuation.result) // Next time this continuation is called, it should go to state 1 continuation.label = 1 // The continuation object is passed to logUserIn to resume // this state machine's execution when it finishes userRemoteDataSource.logUserIn(userId!! , password!! . continuation) } 1 -> { // Checks for failures throwOnFailure(continuation.result) // Gets the result of the previous state continuation.user = continuation.result as User // Next time this continuation is called, it should go to state 2 continuation.label = 2 // The continuation object is passed to logUserIn to resume // this state machine's execution when it finishes userLocalDataSource.logUserIn(continuation.user, continuation) } ... // leaving out the last state on purpose } }Copy the code
Take a moment to look at the code above and see if you can spot any differences from the previous code snippet. Let’s see what the compiler generates.
- The parameter to the WHEN statement is the Label in the LoginUserStateMachine instance.
- Each time a new state is processed, there is a check in case an exception occurs when the function suspend.
- Before invoking the next suspend function (logUserIn), the Label of the LoginUserStateMachine instance is updated to the next state.
- When there is a call to another suspend function inside the state machine, an instance of the continuation (of type LoginUserStateMachine) is passed as a parameter. The suspend function to be called, which has also been converted by the compiler, is another state machine like this one that takes a continuation object as an argument! When the state machine for the suspend function completes, it resumes execution of the state machine.
The last state is different because it must resume the execution of calling this function, which, as you can see in the code, calls resume on the cont variable stored in LoginUserStateMachine (at construction time).
/* Copyright 2019 Google LLC.spdx-license-Identifier: Apache-2.0 */ fun loginUser(userId: String? , password: String? , completion: Continuation<Any? >) {... val continuation = completion as? LoginUserStateMachine ? : LoginUserStateMachine(completion) when(continuation.label) { ... 2 -> { // Checks for failures throwOnFailure(continuation.result) // Gets the result of the previous state continuation.userDb = continuation.result as UserDb // Resumes the execution of the function that called this one continuation.cont.resume(continuation.userDb) } else -> throw IllegalStateException(...) }}Copy the code
As you can see, the Kotlin compiler does a lot for us! Take the suspend function function as an example.
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 all of this for us.
/* Copyright 2019 Google LLC.spdx-license-Identifier: Apache-2.0 */ fun loginUser(userId: String? , password: String? , completion: Continuation<Any? >) { class LoginUserStateMachine( // completion parameter is the callback to the function that called loginUser completion: Continuation<Any? > ): CoroutineImpl(completion) { // objects to store across the suspend function var user: User? = null var userDb: UserDb? = null // Common objects for all CoroutineImpl var result: Any? = null var label: Int = 0 // this function calls the loginUser again to trigger the // state machine (label will be already in the next state) and // result will be the result of the previous state's computation override fun invokeSuspend(result: Any?) { this.result = result loginUser(null, null, this) } } val continuation = completion as? LoginUserStateMachine ? : LoginUserStateMachine(completion) when(continuation.label) { 0 -> { // Checks for failures throwOnFailure(continuation.result) // Next time this continuation is called, it should go to state 1 continuation.label = 1 // The continuation object is passed to logUserIn to resume // this state machine's execution when it finishes userRemoteDataSource.logUserIn(userId!! , password!! . continuation) } 1 -> { // Checks for failures throwOnFailure(continuation.result) // Gets the result of the previous state continuation.user = continuation.result as User // Next time this continuation is called, it should go to state 2 continuation.label = 2 // The continuation object is passed to logUserIn to resume // this state machine's execution when it finishes userLocalDataSource.logUserIn(continuation.user, continuation) } 2 -> { // Checks for failures throwOnFailure(continuation.result) // Gets the result of the previous state continuation.userDb = continuation.result as UserDb // Resumes the execution of the function that called this one continuation.cont.resume(continuation.userDb) } else -> throw IllegalStateException(...) }}Copy the code
The Kotlin compiler converts each suspend function into a state machine, optimized with callbacks each time a function needs suspend.
Now that you know exactly what the compiler does at compile time, you can better understand why a suspend function does not return until it has done all its work. Also, you can see how your code can suspend without blocking the thread — because the information that needs to be executed when the function resumes is stored in a Continuation object!
Reference: medium.com/androiddeve…
I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit