[In-depth understanding of Kotlin coroutines] Creation, startup, and suspension functions of coroutines [Theory]
Kotlin implements the resume suspension point through an interface class Continuation (English translation is called “continuation, continuation, continuation”).
Kotlin continuations have two interfaces: Continuation
and CancellableContinuation, as the name suggests CancellableContinuation is a Continuation that can be canceled.
public interface Continuation {
public val context: CoroutineContext
public fun resumeWith(result: Result)
}
Members of the Continuation:
-
val context: CoroutineContext: CoroutineContext context of the current coroutine
-
fun resumeWith(result: Result): Pass the result to resume the coroutine
CancellableContinuation members:
-
isActive, isCompleted, isCancelled: Indicates the status of the current Continuation
- fun cancel(cause: Throwable? = null): Optionally cancel the execution of the current Continuation through an exception cause
Continuation can be regarded as code encapsulation (state machine implementation) that needs to be executed after the suspension point is resumed, for example, for the following logic:
suspend fun request() = suspendCoroutine {
val response = doRequest()
it. resume(response)
}
fun test() = runBlocking {
val response = request()
handle(response)
}
Use the following pseudocode to briefly describe the work of Continuation:
interface Continuation {
fun resume(t: T)
}
fun request(continuation: Continuation) {
val response = doRequest()
continuation. resume(response)
}
fun test() {
request(object :Continuation{
override fun resume(response: Response) {
handle(response)
}
})
}
For the suspend function modified by the suspend keyword, the compiler will add a Continuation continuation type parameter (equivalent to the callback in CPS), the execution of the coroutine can be resumed by returning the result value of the resume method of the Continuation continuation object.
Creation of coroutines
It is not difficult to create a simple coroutine in kotlin
The standard library provides a createCoroutinue function, through which we can create a coroutine, but this coroutine will not be executed immediately.
Let’s take a look at its declaration first:
fun (suspend () -> T).createCoroutine(completion: Continuation): Continuation
where suspend () -> T is the Receiver of the createCoroutinue function
-
Receiver is a suspending function modified by suspend , which is also the execution body of the coroutine, we might as well call it the body of the coroutine.
-
The parameter completion will be called after the execution of the coroutine is completed, which is actually the completion callback of the coroutine.
- The return value is a Continuation object. Since the coroutine is only created and processed now, it is necessary to use this value to trigger the start of the coroutine later.
Coroutine start
We already know how to create coroutines, so how do coroutines run? After calling continuation.resume(Unit) , the coroutine will start executing immediately.
By reading the source code of createCoroutine or directly interrupting debugging, we can know that continuation is an instance of SafeContinuation , but don’t be fooled by it The appearance is deceiving, it is actually just a “vest”. It has an attribute called delegate , which is the body of Continuation .
If you are familiar with the naming method of anonymous inner classes in Java bytecode, you will guess that this actually refers to a certain anonymous inner class. Then a new question arises, where does the anonymous inner class come from?
The answer is also very simple, it is our coroutine body, the suspend lambda expression used to create a coroutine. After compiling it, the compiler generates an anonymous inner class, which inherits from the SuspendLambda class, and this class is the implementation class of the Continuation interface.
One final point of confusion, how is the suspend lambda expression compiled? How does a function correspond to a class? It is not difficult to understand here, SuspendLambda has an abstract function invokeSuspend (this function is declared in its parent class BaseContinuationImpl ), the compiled anonymous The implementation of this function in the inner class is our coroutine body.
SuspendLambda inheritance relationship:
– Continuation: Continuation, resume the execution of the coroutine
– BaseContinuationImpl: implement the resumeWith(Result) method, control the execution of the state machine, and define the abstract method invokeSuspend
– ContinuationImpl: Add intercepted interceptor, implement thread scheduling, etc.
– SuspendLambda: Encapsulates the coroutine body code block
– The subclass generated by the coroutine body code block: implement the invokeSuspend method, which implements the state machine flow logic
the
Please note that the object contained inside SafeContinuation is the anonymous inner class generated by the compiler, and this anonymous inner class is also a subclass of SuspendLambda .
It seems very clear that the Continuation instance returned by creating a coroutine is a coroutine body with several layers of vests, so calling its resume can trigger the execution of the coroutine body.
Generally speaking, after we create a coroutine, we will let it start executing immediately, so the standard library also provides a one-step API —— startCoroutine. It is exactly the same as createCoroutine except that the return value type is different.
fun (suspend () -> T).startCoroutine(completion: Continuation)
We already know that completion passed in as a parameter is like a callback, and the return value of the coroutine body will be passed in as a parameter of resumeWith .
Receiver of coroutine body
There are two sets of APIs related to the creation and startup of coroutines. In addition to the previous set, there areA group:
Careful comparison reveals that the difference between these two groups of APIs lies only in the type of the coroutine body itself. The coroutine body of this group of APIs has an additional Receiver type R . This R can provide a scope for the coroutine body, and we can directly use the functions or states provided in the scope in the coroutine body.
Kotlin does not provide the syntax of Lambda expressions directly with Receiver. In order to facilitate the use of the coroutine API with Receiver, we can encapsulate a function launchCoroutine to start the coroutine, as shown in Listing 3-2.
The Receiver type of the second parameter of launchCoroutine is actually deduced by the compiler for us, which just solves the problem that the Lambda expression with Receiver cannot be directly declared.
Since the scope ProducerScope is added as Receiver, we can directly call the produce function in the coroutine body in the example. The delay function is a suspending function that we define outside ProducerScope, and it can also be called freely in the coroutine body. (Functions like launchCoroutine to simplify the creation and startup of coroutines are usually called constructors of coroutines. Their function is to construct coroutines, but not to be confused with class constructors)
Scope can be used to provide function support, and of course it can also be used to increase restrictions. If we add a RestrictsSuspension annotation, then under its effect, the coroutine body cannot call the external suspend function, as shown in the code list 3-4.
Here, under the action of the RestrictsSuspension annotation, the external suspension function delay cannot be called inside the coroutine body. This feature is very helpful for many coroutine bodies created in specific scenarios, and can avoid Invalid or even dangerous calls to suspend functions. The Sequence Builder in the standard library uses this annotation.
Suspend function
We already know that a function modified with the suspend keyword is called a suspend function, and suspend function can only be called within a coroutine body or other suspend functions. In this way, the functions in the entire Kotlin language system are divided into two factions: Ordinary functions and Suspension functions. Among them, suspended functions can call any function, and ordinary functions can only call ordinary functions.
Through the above two suspend functions, we found that the suspend function can return synchronously like a normal function (such as suspendFunc01), and can also handle asynchronous logic (such as suspendFunc02). Since it is a function,They also have their own function types, suspend (Int) -> Unit and suspend (String, String) -> Int in turn.
In the definition of suspendFunc02 , we used suspendCoroutine again to obtain the Continuation of the current coroutine body The instance is used as a parameter to treat the suspending function as an asynchronous function, and the newly created thread executes the Continutation.resumeWith operation at ① of Code Listing 3-7, so the coroutine calls suspendFunc02 It cannot be executed synchronously and will enter a suspended state until the result is returned.
The so-called suspension of the coroutine actually means that when the program execution process is called asynchronously, the execution state of the current calling process enters the waiting state. Please note that the suspend function does not necessarily actually suspend, but only provides the conditions for suspending. Then under what circumstances will it really hang?
Hang point
In the previous definition of suspendFunc02, we found that if a function wants to suspend itself, all it needs is an instance of Continuation , and we can indeed pass suspendCoroutine function gets it, but where does this Continuation come from?
Recalling the process of creating and running a coroutine, our coroutine body itself is a Continuation instance, which is why the suspend function runs inside the body of the coroutine. The place where the function is suspended inside the coroutine is called the suspension point. If there is an asynchronous call at the suspend point, the current coroutine will be suspended until the corresponding Continuation ‘s resume function is called to resume execution.
We already know that the Continuation obtained through the suspendCoroutine function is an instance of SafeContinuation, which is the same as when creating a coroutine The resulting Continuation instance used to start the coroutine is not fundamentally different. The role of the SafeContinuation class is also very simple. It can ensure that it will only be suspended when an asynchronous call occurs. For example, the situation shown in Listing 3-8 also has a resume function Called, but the coroutine doesn’t actually hang.
Whether the asynchronous call occurs depends on whether the call of the resume function and the corresponding suspend function are on the same call stack. The method of switching the function call stack can be to switch to another thread for execution, or not to switch the thread but Execute at a certain moment after the current function returns. The former is easier to understand, while the latter usually saves the instance of Continuation first, and then calls it at an appropriate time in the future. Platforms with event loops can easily do this, such as the main thread Looper loop of the Android platform.
CPS transformation
Continuation Passing Style (Continuation Passing Style): A programming specification is agreed. The function does not return the result value directly, but passes in a callback function parameter at the last parameter position of the function, and completes the execution of the function When using callback to process the result. The callback function callback is called Continuation, which determines the next behavior of the program, and the logic of the entire program is spliced together through Continuations.
CPS Transformation (Continuation-Passing-Style Transformation), that is, passing Continuation To control asynchronous call process, and solve the problem of callback hell and stack space occupation .
-
Kotlin suspend suspend functions are written in the same way as ordinary functions, but the compiler will perform CPS transformation on the function of the suspend keyword. This is what we often say to write asynchronous code in a way that looks synchronous, eliminating callback hell ( callback hell).
-
In addition, in order to avoid the problem of excessive stack space, the Kotlin compiler does not convert the code into the form of function callback, but uses the state machine model. Every two suspension points can be seen as a state, every time you enter the state machineThere is a current state, and then the code corresponding to the state is executed; if the program is executed, the result value is returned, otherwise a special value is returned, which means exit from this state and wait for the next entry. It is equivalent to creating a reusable callback, using the same callback every time, and executing different codes according to different states.
The continuation is a relatively abstract concept. Simply put, it wraps the code that the coroutine should continue to execute after it is suspended; during the compilation process, a complete coroutine is divided into continuations one after another. Each suspending function is regarded as a suspending point, and after the suspending function is executed, the execution of the follow-up body is resumed from the suspending point.
Let’s imagine, when the program is suspended, what is the most important thing to do? Is the save suspension point. The thread is similar, when it is interrupted, the interruption point is saved in the call stack.
When the Kotlin coroutine is suspended, the information of the suspension point is saved in the Continuation object. Continuation carries the context needed to continue the execution of the coroutine. When resuming execution, you only need to execute its resume call and pass in the required parameters or exceptions. As an ordinary object, Continuation occupies very little memory, which is an important reason why stackless coroutines are popular.
As we mentioned earlier, if the suspend function needs to be suspended, it needs to get the Continuation instance through suspendCoroutine , we already know that it is a coroutineProgram body, but how did this instance come in?
Let’s still take the code listing 3-8 as an example. The notSuspend function does not seem to receive any parameters. The Kotlin syntax does not seem to tell us any truth. There are two ways to get to the bottom of it: look at the bytecode or use Java The code calls it directly, and the other is to use Kotlin reflection. These two behaviors can almost be considered as violating Kotlin grammar, and they are only used for research and study, and must not be written in a production environment. Let’s first look at how to call the suspend function with Java code, as shown in Listing 3-9.
We found that the function notSuspend of type suspend () -> Int is actually of type (Continuation) -> Object in Java language, which means Just like the asynchronous callback method we wrote, just pass a callback in and wait for the result to return.
But why does the return value Object appear here? Usually our callback method will not have a return value. There are two cases for the return value Object here:
-
The suspend function returns synchronously, resumeWith of the Continuation passed in as a parameter will not be called, the function > The actual return value of the number isIt serves as the return value of the suspending function. Although notSuspend seems to call resumeWith, the calling object is SafeContinuation, which we have mentioned many times before, so its implementation belongs to synchronous return.
-
Suspend function suspends, performs asynchronous logic. At this time, the actual return value of the function is a suspended flag, through which the external coroutine can know that the function needs to be suspended and wait until the asynchronous logic is executed. In Kotlin this flag is a constant defined in Intrinsics.kt:
Now everyone knows that the original suspend function is an extra Continuation instance in the parameters of the ordinary function, no wonder the suspend function always Ordinary functions can be called, but ordinary functions cannot call suspend functions.
Now please think about it carefully, Why does the Kotlin syntax require that the suspend function must run in the coroutine body or other suspend functions? The answer is that there isan implicit Continuation instance in any coroutine body or suspending function, and the compiler can correct this instance pass, and hide this detail behind a coroutine, making our asynchronous code look like synchronous code.
The function of the suspend keyword is added, and kotlin finally generates the following method:
As you can see, the suspend function actually needs a continuation parameter
If the suspend function is not actually suspended (no thread switching occurs), the return value returns the actual parameter type, otherwise it returns a suspension flag.
Fanwai: Implementation principle of suspendable main function in Kotlin:
suspend fun main() {
....
}
The compiler will suspend the mainThe function generates the following equivalent code:
fun main(continuation: Continuation): Any? {
return println("Hello")
}
In fact, in the source code, the runSuspend method is called in the main() function, and a coroutine body is created and started in runSuspend, but we cannot see this code.
fun main() {
runSuspend(::main1 as suspend () -> Unit)
}
It can be understood by looking at the Java code decompiled by kotlin bytecode:
/**
* Wrapper for `suspend fun main` and `@Test suspend fun testXXX` functions.
*/
@SinceKotlin("1.3")
internal fun runSuspend(block: suspend () -> Unit) {
val run = RunSuspend()
block. startCoroutine(run)
run. await()
}
private class RunSuspend : Continuation {
override val context: CoroutineContext
get() = EmptyCoroutineContext
//-Xallow-result-rreturn-type
var result: Result? = null
override fun resumeWith(result: Result) = synchronized(this) {
`` this.result = result
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (this as Object).notifyAll()
}
fun await() = synchronized(this) {
while (true) {
When (val result = this.result) {
null -> @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (this as Object).wait()
else -> {
result. getOrThrow() // throw up failure
return
}
}
}
}
}
It can be seen that it is implemented with Object.wait() and Object.notifyAll().
refer to:
“In-depth understanding of Kotlin coroutines” – 2020 – Mechanical Industry Press – Huo Bingqian
Understanding the working principle of Kotlin coroutines again https://juejin.cn/post/7137905800504148004