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
, await
as 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-core
and integrated with other asynchronous and concurrent technologies, such as: kotlinx-coroutines-jdk8
, kotlinx-coroutines-reactive
, kotlinx-coroutines-rx1/rx2
, kotlinx-coroutines-reactor
etc.
Currently (Kotlin 1.2), Kotlin coroutines are only an experimental feature. Therefore, the package name of Kotlin Coroutine related classes contains experimental
the 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 CompletableFuture
integrate with .
Concatenate asynchronous calls in the traditional way using CompletableFuture
needs 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 CompletableFuture
Coroutine adds await
methods 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, monoA
the 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 lastLogin
and 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 awaitSingle
will cause the program to be thrown NoSuchElementException
, which cannot be directly caught by try...catch (it can only be handled by Mono
the error handling callback method of , such as doOnError
, onErrorCosume
etc.). In order to except the case where the query result may be empty, the awaitFirstOrDefault
method 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
, awaitSingle
etc. 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 suspend
keyword . The following is awaitSingle
an 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 , async
for use with Spring Reactor .mono
flux
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 launch
will not be suspended, you need to use Thread.sleep
to avoid the main thread exiting early)
launch
The 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
launch
Similar to , async
can also be used to start a Coroutine. The difference is that the launch
returned one Job
can only control the execution of the Coroutine, but cannot get any return result. async
What is returned is Deferred
that 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 mono
is different from the previous Coroutine Builders. The most obvious difference is the one in the parentheses at the end Unconfined
. Simply put, this Unconfined
is 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 CoroutineDispatcher
a 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 CoroutineDispatcher
as a parameter to Coroutine Builder?
It turns out that CoroutineDispatcher
implements the CoroutineContext.Element
interface, Element
but is a special one CoroutineContext
, which only stores one element CoroutineContext
. So, CoroutineDispatcher
also one CoroutineContext
. This contains CoroutineContext
only one element, and that element is CoroutineDispatcher
itself.
When the Coroutine is executed, Kotlin will see if CoroutineContext
there CoroutineDispatcher
. If there is, use CoroutineDispatcher
to qualify the threads used by the Coroutine.
When no parameters are specified for the Coroutine Builder, launch
, async
, and mono
and flux
are used CoroutineDispatcher
by CommonPool
, a common thread pool implementation. runBlocking
is used by default BlockingEventLoop
. Another common CoroutineDispatcher
implementation one mono
in the example Unconfined
.
Unconfined
It 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
- Kotlin Coroutine context and dispatchers:https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#coroutine-context-and-dispatchers
- Kotlin CoroutineDispatcher Javadoc:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-coroutine-dispatcher/index.html
- Quasar Fiber Technical Documentation: http://docs.paralleluniverse.co/quasar/
- Alibaba JDK coroutine: http://www.infoq.com/cn/presentations/free-performance-lunch-alibaba-jdk-association/
- Open JDK Project Loom 提案:http://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html!
Welcome to pay attention to my technical WeChat public account "editing and writing"