A First Look at Kotlin Coroutines

I. Introduction

Kotlin is a programming language that has emerged in the past two years, and it has developed rapidly in the past year. In 2017, Google announced Kotlin as the official development language for Android. At the same time, Spring, one of the leading brothers in the field of Java server-side development, also provides comprehensive support for Kotlin.

Among the many features of Kotlin, the Coroutine (coroutine, which can be simply regarded as a lightweight thread) technology added as an experimental feature in 1.1 is very noteworthy. Because everyone knows that the popularity of the Go language, which has become popular in recent years, has a lot to do with its coroutine features, which also makes Java's threading technology seem backward. Because the main way to improve concurrency in traditional Java applications is to open more threads, but too many threads will lead to waste of resources, and too low threads will easily lead to insufficient concurrency. Although technologies such as Netty can solve the concurrency problem in IO-intensive scenarios, the threshold for use is relatively high, the learning curve is relatively steep, and it is not easy to use in large areas.

The Coroutine feature of Kotlin brings a very promising new option for high-concurrency application development on the JVM, so it is worthy of attention. This article will introduce you to what Kotlin Coroutine is, how to use it, and what problems it can solve.

The content of this article is based on Kotlin Coroutine 0.21.2 version.

2. What is Kotlin Coroutine

Kotlin Coroutine is a feature provided by Kotlin to achieve better asynchronous and concurrent programs, introduced since version 1.1. Unlike other programming languages, Kotlin uses most of its Coroutine features as an extension library: kolinx.coroutines, with limited support at the language level.

For example, languages ​​such as C#, ECMAScript, etc. have async, awaitas keywords, but in Kotlin, these are just ordinary methods. In Kotlin, there are only keywords related to Coroutine suspend.

Not only that, Kotlin further splits the Coroutine library into core modules kotlinx-coroutines-coreand integrated with other asynchronous and concurrent technologies, such as: kotlinx-coroutines-jdk8, kotlinx-coroutines-reactive, kotlinx-coroutines-rx1/rx2, kotlinx-coroutines-reactoretc.

Currently (Kotlin 1.2), Kotlin coroutines are only an experimental feature. Therefore, the package name of Kotlin Coroutine related classes contains experimentalthe word . But it's basically a foregone conclusion that Kotlin will officially include the Coroutine feature. According to the current plan, Kotlin 1.3 will officially include the Coroutine feature. At present, the overall design and usage of Coroutine has long been determined and will not change significantly.

3. The use of Kotlin Coroutine

Next, let's see how Kotlin Coroutine solves the problems and pain points we encounter in asynchronous and concurrent programming in different scenarios.

Scenario 1: Delayed Execution

When we are developing, we often encounter scenarios where we need to wait for a period of time before executing certain statements. At this time, we commonly use Thread.sleep to achieve:

@Test
fun delay_with_thread() {
    log.info("Hello")
    Thread.sleep(1000)
    log.info("World")
}

This is inefficient, because the thread wastes a second in vain. If this code is called a lot, it will be a waste of resources.

We can improve this a bit and use ScheduledThreadPool:

@Test
fun delay_with_scheduler() {
    val scheduler = Executors.newScheduledThreadPool(1)
    log.info("Hello")
    scheduler.schedule({
        log.info("World")
    }, 1, TimeUnit.SECONDS)
    scheduler.shutdown()
    scheduler.awaitTermination(1, TimeUnit.SECONDS)
}

Although this is efficient, the disadvantage is also obvious, that is, the code becomes very unintuitive. If the code is more complex, it is even more difficult to understand.

If you use Kotlin Coroutine, how to write it?

@Test
fun delay_with_coroutine() {
    runBlocking {
        log.info("Hello")
        delay(1000)
        log.info("World")
    }
}

Is it very simple, the only difference from the first version is to Thread.sleep(1000)replace it with delay(1000). Moreover, delay(1000)the current thread will not be suspended, so the code execution efficiency is much higher.

Scenario 2: Completable Future

Kotlin Coroutines provide integration with various asynchronous technologies, including JDK8 Completable Future, Google Guava's Listenable Future, Spring's Reactor, Netflix's RxJava, etc., but not Future in JDK5. The reason is that the traditional Future interface does not provide any callback mechanism, so Kotlin Coroutine cannot integrate with it. Therefore, this section focuses on how Kotlin Coroutines CompletableFutureintegrate with .

Concatenate asynchronous calls in the traditional way using CompletableFutureneeds to call thenApply, thenCompose, and methods like this:thenAccept

val future = CompletableFuture.supplyAsync({ 1 })
future.thenApply { value -> "${value + 2}" }
        .thenAccept({ value ->
    log.info(value.toString())
})

Kotlin CompletableFutureCoroutine adds awaitmethods to the interface, which can convert callbacks to traditional invocations:

val future = CompletableFuture.supplyAsync({ 1 })
val value = future.await()
val result = value + 2
log.info(result.toString())

It can be seen that the code has been significantly simplified after using Kotlin Coroutine.

Scenario 3: Reactive Programming

Let's take a look at how Kotlin Coroutines simplify reactive programming.

With the advent of Spring 5, developers can more easily use reactive programming in the web development world, thereby increasing the concurrency performance and scalability of the system. However, while reactive programming techniques like the Spring Reactor project and the Netflix RxJava project have made asynchronous programming a lot easier, they are still far from ideal.

Next, let's take a look at the problems of existing reactive programming technologies and how Kotlin Coroutine solves these problems.

Use Spring Reactor directly

The purpose of the following code is to check how many new messages there are since the last login based on the person ID. It uses the reactive programming features of Spring 5, the Reactor API and the Reactive Repository in Spring Data.

@GetMapping("/reactive/{personId}")
fun getMessagesFor(@PathVariable personId: String): Mono<String> {
  return peopleRepository.findById(personId)
      .switchIfEmpty(Mono.error(NoSuchElementException()))
      .flatMap { person ->
          auditRepository.findByEmail(person.email)
              .flatMap { lastLogin ->
                  messageRepository.countByMessageDateGreaterThanAndEmail(lastLogin.eventDate, person.email)
                      .map { numberOfMessages ->
                          "Hello ${person.name}, you have $numberOfMessages messages since ${lastLogin.eventDate}"
                      }
              }
      }
}

After seeing the above code, I think most people's intuitive feelings are "so complicated", "Callback Hell" and so on.

Wait, isn't it said that the Reactive Stream method can avoid Callback Hell? Why does Callback Hell still exist here. In fact, Reactive Programming frameworks like RxJava and Reactor can solve the Callback Hell problem in a limited range. Generally speaking, if there is a series of calls, each step only depends on the result of the previous step, then the Reactive Stream method can be perfectly written as a chain call:

monoA.flatMap(valueA -> {
  returnMonoB(valueA);
}).flatMap(valueB -> {
  returnMonoC(valueB);
}).flatMap(valueC -> {
  returnMonoD(valueC);
});

In the above code, monoAthe value contained in is valueA, and so on.

But the question is, where is the real business requirement so simple and ideal. Taking the above application of querying the number of new messages as an example, messageRepository.countByMessageDateGreaterThanAndEmail(lastLogin.eventDate, person.email)this step depends on the result of the previous step lastLoginand of the previous step person. Not meeting the condition I said before "each step only depends on the result of the previous step" makes this example not easy to write as a perfect chain of calls.

Although the above code can be optimized to a certain extent through some small tricks, the readability after optimization is still not high.

Using Kotlin Coroutines

Spring 5 provides complete support for Kotlin. Likewise, Kotlin has also added support for Spring. One of them is support for Spring Reactor projects. So we can use Kotlin Coroutine to transform the above code:

@GetMapping("/coroutine/{personId}")
fun getNumberOfMessages(@PathVariable personId: String) = mono(Unconfined) {
    val person = peopleRepository.findById(personId).awaitFirstOrDefault(null)
            ?: throw NoSuchElementException("No person can be found by $personId")

    val lastLoginDate = auditRepository.findByEmail(person.email).awaitSingle().eventDate

    val numberOfMessages =
            messageRepository.countByMessageDateGreaterThanAndEmail(lastLoginDate, person.email).awaitSingle()

    "Hello ${person.name}, you have $numberOfMessages messages since $lastLoginDate"
}

The most obvious change in the code after the renovation is that the code readability has improved a lot. The readability of the code is very important for all software systems. If the code is difficult for people to understand, the cost of maintaining and upgrading the software system will be very high. Therefore, Kotlin Coroutine's improvement in code readability for asynchronous programming is very valuable.

Description: If the query result is empty, calling awaitSinglewill cause the program to be thrown NoSuchElementException, which cannot be directly caught by try...catch (it can only be handled by Monothe error handling callback method of , such as doOnError, onErrorCosumeetc.). In order to except the case where the query result may be empty, the awaitFirstOrDefaultmethod is used .

4. Explanation

Some of the benefits of using Kotlin Coroutines are described above. Next, the above code and important concepts in Kotlin Coroutine will be introduced.

suspending method

Summarizing the characteristics of Kotlin Coroutine in one sentence can be "in the name of synchronization, the reality of asynchronous". So how does this "real" work? The key is the suspending method. The above Kotlin Coroutine examples have multiple suspending methods: delay, await, awaitSingleetc. These suspending methods can suspend program execution without suspending the thread. This makes the program both efficient and easy to understand.

The declaration of a suspending method is simple, just add the suspendkeyword . The following is awaitSinglean example :

public suspend fun <T> Publisher<T>.awaitSingle(): T = awaitOne(Mode.SINGLE)

The suspending method is easy to declare, but the use of the suspending method is limited, and the suspending method cannot be called anywhere. The suspending method can only be called in two places, one is in another suspending method, and the other is in the Coroutine Builder. So, let's take a look at what Coroutine Builder is next.

Coroutine Builder

Coroutine Builder, as the name suggests, is used to create Coroutines. As for how Coroutine Builder creates Coroutine, I will talk about it in a later article. Let's first take a look at what Coroutine Builders are and how they are used.

Common Coroutine Builders are runBlocking, launch, , and , asyncfor use with Spring Reactor .monoflux

Simply put, Coroutine Builders are methods that accept a suspending lambda as a parameter and execute it in a Coroutine. A complete Coroutine call starts with a Coroutine Builder.

Briefly talk about the usage of several common Coroutine Builder:

runBlocking

The role of this Coroutine Builder is to block the thread calling it. For example, in the delay example above, runBlocking is used.

launch

This Coroutine Builder will create a Coroutine and execute it, and return a Job object, which is used to control the execution of this Coroutine, but no result is returned.

For example, the previous delay example can also be written like this

fun main(args: Array<String>) {
    launch { // launch new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

(Note: Because the thread launchwill not be suspended, you need to use Thread.sleepto avoid the main thread exiting early)

launchThe method returns a Job object:

@Test
fun delay_with_coroutine_launch() {
    runBlocking {
        log.info("Hello")
        val job = launch {
            // launch new coroutine and keep a reference to its Job
            delay(5000L)
            log.info("World")
        }
        job.cancel()
        job.join() // wait until child coroutine completes
    }
}

The Job object provides methods such as cancel(), join()to control the execution of the Job (because the join()method is also a suspending method, so a layer is added outside runBlocking)

async

launchSimilar to , asynccan also be used to start a Coroutine. The difference is that the launchreturned one Jobcan only control the execution of the Coroutine, but cannot get any return result. asyncWhat is returned is Deferredthat you can get the running result of the Coroutine by calling its provided await()method :

@Test
fun delay_with_async() {
    log.info("Start to demo async")

    val one = async {
        delay(1000)
        1
    }

    val two = async {
        delay(2000)
        2
    }

    runBlocking { log.info("${one.await() + two.await()}") }
}

mono sum flux

The last two Coroutine Builders introduced are provided by kotlinx-coroutines-reactor for integration with Spring's Reactor project. From the above example, it monois different from the previous Coroutine Builders. The most obvious difference is the one in the parentheses at the end Unconfined. Simply put, this Unconfinedis one CoroutineDispatcher, which is used to limit what thread is used to execute Coroutine.

In the documentation of the Kotlin project on Github, there is CoroutineDispatchera detailed description of (link is given at the end). Next, I will explain the contents of the document to facilitate your understanding.

The first parameter of all Coroutine Builder methods is CoroutineContext. So why can you pass it CoroutineDispatcheras a parameter to Coroutine Builder?

It turns out that CoroutineDispatcherimplements the CoroutineContext.Elementinterface, Elementbut is a special one CoroutineContext, which only stores one element CoroutineContext. So, CoroutineDispatcheralso one CoroutineContext. This contains CoroutineContextonly one element, and that element is CoroutineDispatcheritself.

When the Coroutine is executed, Kotlin will see if CoroutineContextthere CoroutineDispatcher. If there is, use CoroutineDispatcherto qualify the threads used by the Coroutine.

When no parameters are specified for the Coroutine Builder, launch, async, and monoand fluxare used CoroutineDispatcherby CommonPool, a common thread pool implementation. runBlockingis used by default BlockingEventLoop. Another common CoroutineDispatcherimplementation one monoin the example Unconfined.

UnconfinedIt means unlimited. Before the first pause point, the execution thread of the Coroutine is the calling thread. After the first suspension point, which thread to execute is determined by the suspending method.

For example, in the "reactive programming" example, peopleRepository.findById(personId)the execution of is using the calling thread. The subsequent execution is using the Mongo async client callback thread (where the Repository is based on the Mongo async client).

V. Summary

Nowadays, in the face of high-concurrency application development scenarios, Java's traditional threading model is becoming more and more powerless. The Java community is also aware of this problem, and a number of projects have emerged that provide lightweight threading solutions, such as the Quasar project, the Alibaba JDK's coroutine solution, the Open JDK Project Loom proposal, and also reactive programming techniques. But these programs all have problems of one kind or another.

The emergence of Kotlin Coroutine provides a new option for solving Java high-concurrency application development and brings new hope. But we also need to see that Kotlin Coroutines are just getting started and have a long way to go. At the same time, although Kotlin Coroutine simplifies the development of asynchronous code in form, it also puts forward considerable requirements for users. If you lack sufficient understanding of Java concurrency, NIO, reactive programming, and Kotlin itself, it may be difficult to use Kotlin Coroutine smoothly. This may also be a historical burden that Java program development is difficult to get rid of.

This article provides a brief introduction to the concepts and usage scenarios of Kotlin Coroutines, the benefits of using Kotlin Coroutines, and some key concepts. Subsequent articles will give you a detailed introduction to Kotlin Coroutine, its implementation principle and the comparison of Kotlin Coroutine with other similar technologies.

Some examples in this article use logs. Here, we remind you that you need to pay attention to avoid the log blocking thread problem in actual projects. Avoiding thread blocking is a concern for almost all high-performance asynchronous application development.

Attachment: Reference

Welcome to pay attention to my technical WeChat public account "editing and writing"

My technical public account "editing and writing"

Guess you like

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