Top-level implementation of coroutines -CPS
The following code is available:
fun test(a: Int, b: Var result = a + b + 2 result = result SHL 1 // + 2 result += 2 println(result)}Copy the code
Let’s SRP the code (single responsibility):
// add fun sum(a: Int,b: Int) = a + b // double(x: Int) = x SHL 1 // add2 fun add2(x: x) Int = x + 2 // Final test fun test(a: Int, b: Int) {println(add2(double(sum(a,b))))})Copy the code
So you can see that we’ve split up a bunch of methods into several methods, each of which does one thing, which makes it more readable and more maintainable, but the code is more complicated, so let’s make it a little bit more complicated.
The code above passes the return value of the inner method as an argument to the outer method. Now we pass the outer method as an interface callback to the inner method:
Fun sum(a: Int, b: Int, next: (Int) -> Unit) = a + b // x times 2 fun double(x: Int, next:) (Int) -> Unit) = x SHL 1 // x + 2 fun add2(x: Int, next: (Int) -> Unit) = x + 2 Int) {// Add sum(a, Double (sum) {double -> add2(double) {result -> // println(result)}}}}Copy the code
This is CPS code style: this is done through interface callbacks.
Hypothesis: We have several methods above: Sum (), double(), and add2() are all suspended functions, which eventually compile to cps-style callbacks, meaning that the code that looks synchronous is “modified” by the compiler to be asynchronous. This is the top-level implementation logic of the Kotlin coroutine.
Now, let’s verify this by defining a suspend function and decommounting it to see if it’s really CPS.
Suspend fun test(id: String): String = "hello"Copy the code
The decompilation results are as follows:
Public Final Object test(@notnull String ID, @NotNull Continuation $completion) { return "hello"; }Copy the code
As you can see, there is a Continuation argument, which is an interface for the callback to be executed after this function completes execution.
Public interface Continuation<in T> {// Save the context (such as variable state) public val context: CoroutineContext public fun resumeWith(result: result <T>)} public fun resumeWith(result: result <T>)}Copy the code
Ok, now that we know that the suspend function is implemented by adding continuations, let’s look at a specific business:
Suspend fun getInfo(id: String): String = "token" info suspend fun getInfo(token: String): Val token = getToken("123"); val token = getToken("123"); val token = getToken("123"); Val info = getInfo(token) // Print println(info)}Copy the code
The business code above is simple, but the first two steps are time-consuming operations. Will the thread be stuck waiting there? CPS = suspend (); CPS = suspend ();
Fungettoken (id: String, callback: Continuation<String>): String = "token" // Same as above, pass the Continuation callback fun getInfo(token: String, callback: Continuation<String>): String = "info" // test(only write mainline code) fun test() {// getToken first, pass the callback getToken("123", object: Continuation<String> { override fun resumeWith(result: Var token = result.getorNULL () getInfo(token!! , object : Continuation<String> { override fun resumeWith(result: Result<String>) {val info = result.getorNULL () println(info)}})}Copy the code
This is CPS style code without suspend, which implements the synchronous code style of coroutines through incoming interface callbacks.
Next, we decomcompile the suspend style code to see how it is scheduled.
The underlying implementation of coroutines – state machines
Let’s start by briefly modifying the suspend test function:
Suspend Fun getInfo(id: String): String = "token" String = "info" // add local variable a, Look at how to save a suspend this variable suspend fun test () {val token = getToken (" 123 ") / / hang starting point 1 var a = 10 / / here is 10 val info = Println (info) println(a) println(a) println(a)Copy the code
Each point of the suspend function call generates a suspend point at which the current running state, such as local variables, is stored.
The decompiled code looks like this:
public final Object getToken(String id, Continuation completion) { return "token"; } public final Object getInfo(String token, Continuation completion) { return "info"; } // Public final Object test(Continuation<String>: continuation) { Continuation cont = new ContinuationImpl(continuation) { int label; // Save state Object result; // save the intermediate Result, remember that the Result<T> is a generic, because the generic erase, so Object, use the strong int tempA; // Save the value of context A, which is generated according to the specific code}; switch(cont.label) { case 0 : { cont.label = 1; // Update label getToken("123",cont) // perform the corresponding operation. } case 1 : { cont.label = 2; Int a = 10; int a = 10; cont.tempA = a; String token = (Object)cont.result; String token = (Object)cont.result; getInfo(token, cont); // Execute the corresponding operation break; } case 2 : { String info = (Object)cont.result; Println (info); Int a = cont.tempa; println(a); return; }}}Copy the code
We can think of each case as a state, and the statement for each case branch as a Continuation implementation.
The above pseudocode roughly describes the scheduling flow of coroutines:
- When we call test, we pass in a Continuation interface, which we’ll redecorate.
- 2 decoration is adding additional context data and state information (that is, label) to the inside according to the function-specific logic.
- 3 Each state corresponds to a Continuation interface in which the corresponding business logic is executed.
- 4 For each state, save the context information, obtain the result of the previous state, execute the business logic of the current state, and restore the context information.
- 5 Until the logic corresponding to the last state is executed.
conclusion
To sum up, we can sum up the following points:
- 1. Kotlin coroutine does not frequently switch threads at the bottom layer, so it does not involve the switch between user state and core mentality. It is implemented at the top level through scheduling, so the efficiency is relatively high.
- In Kotlin, each suspend method requires a Continuation interface implementation to perform the next state operation; Also, each invocation point of the suspend method produces a suspend starting point.
- 3. Each suspension point generates a label corresponding to a state of the state machine. The different states are switched with continuations.
- The Kotlin coroutine saves the current context data at each hang point and restores it after the hang point. In this way, each state is independent of each other and can be scheduled independently.
- 5. The switching of coroutines is just switching from one state to another. Since different states are independent of each other, switching back at a proper time will not affect the results.