Kotlin Coroutine Principle Analysis

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, createPostetc. 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 suspendkeyword , 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:

  1. suspending method and Continuation
  2. CPS transition and Switch state machine
  3. suspendCoroutine method
  4. CoroutineBuilder method

##(1) Suspending method metamorphosis

The definition of the suspending method is very simple, just add the suspendkeyword . But the Java platform does not have suspendkeywords , 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 Continuationtype input parameter, and the return value becomes Object
  • Processing 2: ContinuationGenerate 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 Continuationtype

fun postItem(item: Item, cont: Continuation): Any?

▼ After the suspending method is compiled, Continuationadd 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 requestTokenmethods , Continuation is createPostand processPostmethods . In common CPS, the Continuation part will be implemented in the callback interface.

In Kotlin Coroutine, Continuation has a more specific meaning - Continuationinterface. 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, Continuationa callback interface is defined. resumeThe 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 postItemmethod of the anonymous inner class of the Continuationtype the resumemethod will call back the postItemmethod 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 Continuationthe recall postItemmethod.

Summary: Each suspending method will add a parameter of Continuationtype . Each suspending method has its own Continuationimplementation , and this class will be passed to other suspending methods called by this suspending method. These sub-methods can Continuationcall back the parent method to resume the suspended program.

A few questions arise here:

  1. What is Pause? How does it happen?
  2. ContinuationHow 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 sminstance , which is then passed smas a parameter of Continuationtype to the child suspending method in the current case.

The child suspending methods (in this case, requestToken, createPostetc.) will smset into the callback interface. When the callback occurs and the child suspending method finishes executing, it smwill call back its corresponding suspending method (in this example postItem), and execute the statement in the corresponding case according labelto 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 = itemparameter itemis saved in the state machine instance sm(type ThisSM, which implements the Continuationinterface ) through the statement, so that subsequent calls can Continuatonobtain the input parameter through .

Then sm.label = 1set . It can also be seen from the subsequent code that in each case, will be sm.labelset 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 requestTokenmethod . You can see that after compiling, requestTokenthere is an additional Continuationtype of input parameter.

case 1

When the requestTokenset callback is triggered (corresponding to the return of the method in Direct Style), the smcallback postItemmethod is passed. At this point, label=1, so case 1 is executed.

By calling ,val item = sm.item get parameters from .smitem

By calling the val token = sm.result as Tokenget requestTokenmethod's return value token.

sm.label = 2Set 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 PostResultContinuationGet 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 , Continuationit 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 :ContinuationContinuationContinuationCoroutineImplCoroutineImpl

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)
  }
}

CoroutineImplThe constructor has an input parameter of Continuationtype completion, which completionrepresents the parent Continuation. The calling resumemethod will be called first processBareContinuationResume. processBareContinuationResumeThe first input parameter is the parent Continuation, and the second input parameter blockis the doResumemethod, 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 Continuationand return the execution result.

Take a look at the flowchart:

Kotlin Coroutine Suspending method parent-child call

4. Summary

The Kotlin Coroutine suspending method changes significantly after compilation:

First, the suspending method adds an input parameter of Continuationtype 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 Continuationan 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 Continuationanonymous 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 Continuationregister 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 - suspendCoroutineimplementation. suspendCoroutineMethods are part of the Kotlin standard library and can be found in kotlin-stdlibmodules in CoroutinesLibrary.kt.

suspendCoroutineThe signature of the method is as follows:

suspend fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T

suspendCoroutineThe input parameter is blocka . This Lambda can have Continuationan . If you can get Continuationit, 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 suspendCoroutinemake to CompletableFutureintegrate 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 suspendCoroutineand 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 suspendCoroutinethe input parameter of blockLambda has an input parameter of Continuationtype , which makes it possible to use the suspendCoroutinemethod to integrate with various Future mechanisms.

To further observe suspendCoroutinethe implementation principle of , suspendCoroutinecall the suspendCoroutineOrReturnmethod , but directly observe suspendCoroutineOrReturethe 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")

suspendCoroutineOrReturnIt 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

suspendCoroutineA 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, runBlockingbeing able to suspend the current thread, and monoconverting Coroutines to Monotypes . 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 monoexample :

fun <T> mono(
    context: CoroutineContext = DefaultDispatcher,
    parent: Job? = null,
    block: suspend CoroutineScope.() -> T?
)

The last parameter blockis 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 Continuationan , but the Lambda itself Continuationis 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 createCoroutineUncheckedmethod .

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.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325256064&siteId=291194637