The previous article "A Preliminary Study of Kotlin Coroutine" introduced the origin, important concepts and usage of Kotlin Coroutine. In order to eliminate everyone's doubts about Kotlin Coroutine and help you better understand and use Kotlin Coroutine, this article will introduce the implementation principle of Kotlin Coroutine on the Java platform.
The examples in the content below are from the video "KotlinConf 2017 - Deep Dives into Coroutines on JVM", but modified.
1. Example
Let's start with a code example, let's say we have the following code:
fun postItem(item: Item): PostResult {
val token = requestToken()
val post = createPost(token, item)
val postResult = processPost(post)
return postResult
}
We don't need to delve into the meaning of this code, just focus on the form of the code. The form of this code is our most common, a method, calling several sub-methods, and finally returning the result. This style is called Direct Style, or Imperative Style. The advantage of this style is that it intuitively reflects business logic, but there are problems in execution efficiency. If the code contains IO-intensive operations, because Direct Style code is often executed synchronously by threads, the thread executing this code will be blocked, resulting in low efficiency. When such code faces a scenario where IO operations take a long time and the amount of concurrency is high, problems will arise, which will then affect the performance of the entire system.
If you want to make the code more suitable for high concurrency and IO intensive scenarios, you need to use Callback style code:
fun postItem(item: Item) {
requestToken { token ->
createPost(token, item) { post ->
processPost(post) { postResult ->
handleResult(postResult)
}
}
}
}
But the problem with Callback style code is that it is ugly, hard to write and hard to debug. Although the execution efficiency is improved, the development efficiency is greatly reduced. This is a serious problem in the face of complex business scenarios. The ideal situation is to be able to use Direct Style to write code as efficient as Callback style.
The emergence of Kotlin Coroutine provides an ideal solution for solving the above problems on the Java platform. With only a small modification, the ideal results mentioned above can be obtained.
▼ Example 1: suspending method version postItem
(assuming requestToken
, createPost
etc. methods are also suspending methods)
suspend fun postItem(item: Item): PostResult {
val token = requestToken()
val post = createPost(token, item)
val postResult = processPost(post)
return postResult
}
As can be seen from the above example, using Kotlin Coroutine, just adding the suspend
keyword , can achieve the same efficiency as the Callback style.
Regarding the use of Kotlin Coroutine, the previous article "Introduction to Kotlin Coroutine" has already introduced it in more detail, so I won't repeat it. Today, let's talk about how Kotlin Coroutine is implemented and what the principle is.
#2. Principle
The suspending method is the main form of using Kotlin Coroutines. The implementation of suspending method depends on various technologies that provide Callback mechanism, such as CompletableFuture of JDK8, ListenableFuture of Google Guava, Spring Reactor, Netflix RxJava, etc. This is why only these technologies can be integrated with Kotlin Coroutines.
Next, we explain how Kotlin Coroutine implements execution pause mechanism without thread blocking based on these techniques. This needs to start with multiple concepts and principles of Kotlin Coroutine:
- suspending method and Continuation
- CPS transition and Switch state machine
- suspendCoroutine method
- CoroutineBuilder method
##(1) Suspending method metamorphosis
The definition of the suspending method is very simple, just add the suspend
keyword . But the Java platform does not have suspend
keywords , and obviously there is no suspending mechanism. How does the suspending method work?
It turns out that the Kotlin compiler will do special processing to the suspending method and convert the code to realize the suspending mechanism.
What processing does the Kotlin compiler do? To put it simply, the following three processes are mainly done:
- Process 1: Add the
Continuation
type input parameter, and the return value becomes Object - Processing 2:
Continuation
Generate anonymous inner class of type - Process 3: The call to the suspending method becomes a state machine in the form of a switch
Next, we describe these three processes in detail
Let's take a look at what the suspending method in example 1 looks like after compiling, so that you can have an overall impression (for the convenience of demonstration, no bytecode is used)
▼ Example 2: How the suspending version postItem method looks after compilation
fun postItem(item: Item, cont: Continuation): Any? {
val sm = cont as? ThisSM ?: object : ThisSM {
fun resume(…) {
postItem(null, this)
}
}
switch (sm.label) {
case 0:
sm.item = item
sm.label = 1
return requestToken(sm)
case 1:
val item = sm.item
val token = sm.result as Token
sm.label = 2
return createPost(token, item, sm)
case 2:
val post = sm.result as Post
sm.label = 3
return processPost(post, sm)
case 3:
return sm.result as PostResult
}
###1. Continuation: method parameters and anonymous inner classes
The changes mentioned in the first and second items can be seen from the above code.
▼ The suspending method adds Continuation
type
fun postItem(item: Item, cont: Continuation): Any?
▼ After the suspending method is compiled, Continuation
add an anonymous inner class of type
val sm = cont as? ThisSM ?: object : ThisSM {
fun resume(…) {
postItem(null, this)
}
}
Both of these refer to a concept - Continuation, so let's introduce it next.
The name Continuation comes from CPS (Continuation-Passing-Style). CPS refers to a programming style. The name CPS looks cool, but it's a Callback style to put it bluntly. The literal translation of Continuation is the continuum, which means the subsequent part. For requestToken
methods , Continuation is createPost
and processPost
methods . In common CPS, the Continuation part will be implemented in the callback interface.
In Kotlin Coroutine, Continuation has a more specific meaning - Continuation
interface. Let's take a look at its interface definition first:
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resume(value: T)
public fun resumeWithException(exception: Throwable)
}
As can be seen from the above code, Continuation
a callback interface is defined. resume
The method is used to resume execution of a suspended Coroutine.
How to resume execution of a suspended Coroutine? As you can see from the above sample code, the postItem
method of the anonymous inner class of the Continuation
type the resume
method will call back the postItem
method itself (but the input parameters have changed, which will be explained later). And, this is passed in the invocation of the suspending method it calls Continuation
, and subsequent methods can pass Continuation
the recall postItem
method.
Summary: Each suspending method will add a parameter of Continuation
type . Each suspending method has its own Continuation
implementation , and this class will be passed to other suspending methods called by this suspending method. These sub-methods can Continuation
call back the parent method to resume the suspended program.
A few questions arise here:
- What is Pause? How does it happen?
Continuation
How and when is the callback interface called?
For these questions, subsequent chapters will provide answers.
###2. Switch state machine
As you can see from the code in Example 2, after the suspending method is compiled, it will turn the original method body into a state machine consisting of a switch statement:
switch (sm.label) {
case 0:
sm.item = item
sm.label = 1
return requestToken(sm)
case 1:
val item = sm.item
val token = sm.result as Token
sm.label = 2
return createPost(token, item, sm)
case 2:
val post = sm.result as Post
sm.label = 3
return processPost(post, sm)
case 3:
return sm.result as PostResult
What's the reason for doing this? As mentioned earlier, the operation of Kotlin Coroutine relies on various Callback mechanisms. That is to say, when a suspending method is called to the end, it is actually registering a callback. The execution result of the method is processed through this callback. When the callback is registered, the current thread does not need to wait any longer. The next step is to return the method, ending the call. So, as you can see in this switch statement, each case will return.
So, the answer to the question in the previous section "What is a pause? How does it happen?" is that the method returns.
Is it very simple. But the method return is only the end of the thread execution level, the function of the entire suspending method has not been completed, the subsequent methods still need to be called, and the execution results still need to be returned. How are these jobs accomplished?
In the sample code above, there are calls for every case sm.label = N
(except the last case). Here N represents the value of the case corresponding to the next case (next step) of the current case. This value is recorded in the sm
instance , which is then passed sm
as a parameter of Continuation
type to the child suspending method in the current case.
The child suspending methods (in this case, requestToken
, createPost
etc.) will sm
set into the callback interface. When the callback occurs and the child suspending method finishes executing, it sm
will call back its corresponding suspending method (in this example postItem
), and execute the statement in the corresponding case according label
to the value in . Thereby, the recovery of program execution is realized.
The above paragraphs explain how the suspending method is suspended and how to resume it.
Next, the code of Example 2 is explained line by line to help you understand more fully:
case 0
First, in case 0, the input sm.item = item
parameter item
is saved in the state machine instance sm
(type ThisSM, which implements the Continuation
interface ) through the statement, so that subsequent calls can Continuaton
obtain the input parameter through .
Then sm.label = 1
set . It can also be seen from the subsequent code that in each case, will be sm.label
set to the value of the next case, so that when passing the Continuation
(that is sm
) callback, you will know which method to call next.
The next step is to call the requestToken
method . You can see that after compiling, requestToken
there is an additional Continuation
type of input parameter.
case 1
When the requestToken
set callback is triggered (corresponding to the return of the method in Direct Style), the sm
callback postItem
method is passed. At this point, label=1
, so case 1 is executed.
By calling ,val item = sm.item
get parameters from .sm
item
By calling the val token = sm.result as Token
get requestToken
method's return value token
.
sm.label = 2
Set the label to the next case by calling .
transfercreatePost(token, item, sm)
case 2
Similar to the content of case 1, omitted.
case 3
return sm.result as PostResult
Continuation
Get the return value from .
###3. Parent-child invocation of Continuation
The previous section explained how the suspending method is suspended and how to resume it. But there is one detail that is not explained: how does a suspending method know whether it should call back the current suspending method or the previous suspending method?Continuation
To explain this, we need to explain a detail that the above example hides. When a suspending method creates its corresponding , Continuation
it will import the newly created one asContinuation
the parent passed in from the input parameter . Since each suspending method creates is based on , take a look at the source code for :Continuation
Continuation
Continuation
CoroutineImpl
CoroutineImpl
abstract class CoroutineImpl(
arity: Int,
@JvmField
protected var completion: Continuation<Any?>?
) : Lambda(arity), Continuation<Any?> {
override fun resume(value: Any?) {
processBareContinuationResume(completion!!) {
doResume(value, null)
}
}
}
fun processBareContinuationResume(completion: Continuation<*>, block: () -> Any?) {
try {
val result = block()
if (result !== COROUTINE_SUSPENDED) {
@Suppress("UNCHECKED_CAST")
(completion as Continuation<Any?>).resume(result)
}
} catch (t: Throwable) {
completion.resumeWithException(t)
}
}
CoroutineImpl
The constructor has an input parameter of Continuation
type completion
, which completion
represents the parent Continuation
. The calling resume
method will be called first processBareContinuationResume
. processBareContinuationResume
The first input parameter is the parent Continuation
, and the second input parameter block
is the doResume
method, which is the call to the current suspending method. If the return result of the current suspending method is not COROUTINE_SUSPENDED
, that is, when the execution is successful, it will call back to completion.resume(result)
the Continuation
and return the execution result.
Take a look at the flowchart:
4. Summary
The Kotlin Coroutine suspending method changes significantly after compilation:
First, the suspending method adds an input parameter of Continuation
type to implement the callback. The return value becomes Object type, which can represent both the real result and the execution state of the Coroutine.
Then, the compiler will generate Continuation
an anonymous inner class (extension CoroutineImpl
) of type for the suspending method, which is used to call back the suspending method itself, and can call back the parent of the previous level after the suspending method is executed. method.
Finally, if this suspending method calls other suspending methods, these calls will be converted into a state machine in the form of a switch, and each case represents a call to a suspending sub-method or the final return. At the same time, the generated Continuation
anonymous inner class will save the label value of the suspending method that needs to be called next, indicating which case in the switch should be executed, thus connecting the whole calling process.
##(2) suspendCoroutine method
The previous content explains how the suspending method implements execution suspension without thread blocking. This is the main part of Kotlin Coroutine - the implementation principle of the suspending method. But the method call has a beginning and an end. Where does the suspending method call end?
Because as mentioned earlier, Kotlin Coroutine is still based on the Callback mechanism. Therefore, when the suspending method is called at the end, it should Continuation
register of some Future technology.
But it can't be accessed at all in the ordinary Suspending method Continuation
, so how to do it?
The way is through a special suspending method - suspendCoroutine
implementation. suspendCoroutine
Methods are part of the Kotlin standard library and can be found in kotlin-stdlib
modules in CoroutinesLibrary.kt
.
suspendCoroutine
The signature of the method is as follows:
suspend fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T
suspendCoroutine
The input parameter is block
a . This Lambda can have Continuation
an . If you can get Continuation
it, you can register it with some Future mechanism.
Take a look at an example from the official Kotlin Coroutine documentation that demonstrates how to use suspendCoroutine
make to CompletableFuture
integrate with Kotlin Coroutines:
suspend fun <T> CompletableFuture<T>.await(): T =
suspendCoroutine<T> { cont: Continuation<T> ->
whenComplete { result, exception ->
if (exception == null) // the future has been completed normally
cont.resume(result)
else // the future has completed with an exception
cont.resumeWithException(exception)
}
}
Note: The above code is just a demonstration suspendCoroutine
and an example of how to integrate with Future technology. While the principle is the same, the real code will be more complex.
As can be seen from the above code, it is precisely because suspendCoroutine
the input parameter of block
Lambda has an input parameter of Continuation
type , which makes it possible to use the suspendCoroutine
method to integrate with various Future mechanisms.
To further observe suspendCoroutine
the implementation principle of , suspendCoroutine
call the suspendCoroutineOrReturn
method , but directly observe suspendCoroutineOrReture
the implementation of the source code that cannot be understood:
inline suspend fun <T> suspendCoroutineOrReturn(crossinline block: (Continuation<T>) -> Any?): T =
throw NotImplementedError("Implementation is intrinsic")
suspendCoroutineOrReturn
It only acts as a marker, and the implementation details are hidden in the compilation phase. But its implementation is different from the ordinary suspending method, so it is necessary to define a special method to treat it differently.
##(3) Coroutine Builder method
suspendCoroutine
A method can be seen as the end point of a Kotlin coroutine call, and the next thing to discuss is the start point of a Kotlin coroutine call. Because the suspending method cannot be called directly by the normal method. If the normal method wants to call the suspending method, it must pass the Coroutine Builder.
The Kotlin Coroutine core and extension modules provide various Coroutine Builders. These Coroutine Builders serve different purposes. For example, runBlocking
being able to suspend the current thread, and mono
converting Coroutines to Mono
types . The role of these different Coroutine Builders is beyond the scope of this article (which will be introduced in subsequent articles), but to introduce the common part of these Coroutine Builders - suspending Lambda.
Take for mono
example :
fun <T> mono(
context: CoroutineContext = DefaultDispatcher,
parent: Job? = null,
block: suspend CoroutineScope.() -> T?
)
The last parameter block
is a suspending Lambda. Like the suspending method, after the suspending lambda is compiled, its body will also be converted into a state machine in the form of a switch. Unlike the handling of suspending methods, the compiler does not produce Continuation
an , but the Lambda itself Continuation
is implemented as (each Lambda generates an anonymous inner class after compilation).
In addition to the processing of suspending Lambda, another common processing of Coroutine Builder is to create a new Coroutine by calling the createCoroutineUnchecked
method .
3. Summary
So far, the main implementation principle of Kotlin Coroutine has been introduced. But there are many other details, you can learn about Kotlin Coroutine official documentation (address: https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md#implementation-details ) and video "KotlinConf 2017 - Deep Dives into Coroutines on JVM" (Address: https://www.youtube.com/watch?v=YrrUCSi72E8 ).
From the development trend of the industry, reactive programming is the main choice for the Java community to deal with high concurrency scenarios, but there are still many inconveniences in using reactive programming technologies (Spring Reactor, RxJava) directly (in the previous article "Kotlin Coroutine" It has been introduced in the Preliminary Exploration). Therefore, the emergence of Kotlin Coroutine solves these problems in a timely and effective manner.
Therefore, it is foreseeable that Kotlin Coroutines will increasingly appear in applications in the fields of Java server-side and Android. Therefore, it makes sense to understand the implementation principle of Kotlin Coroutine.
In addition, Coroutine is not an invention of Kotlin, and many other languages have the concept of Coroutine, such as LISP, Python, Javascript, etc. The implementation principle of Kotlin also draws on many other languages. Therefore, understanding the principle of Kotlin Coroutine can also help understand the underlying principles of Coroutine technology in other languages.
This article introduces the implementation principle of Kotlin Coroutine is here. Subsequent articles related to Kotlin Coroutine will introduce the integration of Kotlin Coroutine and Spring Reactor projects, the comparison of Kotlin Coroutine and Quasar, Alibaba JDK and other technical solutions, and so on. Please pay attention.