Simon Wirtz in his blog publishes a lot of interesting posts about Kotlin.
I present to you the translation of one of them.
As I mentioned on Twitter a few days ago, I planned to take a closer look at the Kortlin Korutin, which I did. But unfortunately, it took longer than I expected. For the most part, this is due to the fact that corintins are a very voluminous topic, especially if you are not familiar with their concept. Anyway, I want to share my view with you and I hope to give you a comprehensive review.
Korutin is undoubtedly one of the “big features,” as stated in the JetBrains blog :
We all know that it is bad to block under load, that GO is set as an example almost everywhere, and that the world is becoming increasingly asynchronous and based on the processing of notifications. Many languages ​​(starting with C # in 2012) support out-of-the-box asynchronous programming using constructs such asasync/await
. In Kotlin, we proceeded from the fact that such constructions can be declared in the library, and async will not be a keyword, but a simple function. This design allows you to implement a different asynchronous API:future/promises
,callback-passing
, etc. It also has everything you need to implement lazy generators (yield
) and some other functionalities.
In other words, cortinos were introduced for the simple implementation of multi-threaded programming. Surely many of you have worked with Java , its Thread-class and classes for multi-threaded programming. I myself have worked a lot with them and am convinced of the maturity of their decisions.
If you are still experiencing difficulty with threads and multithreading in Java , then I recommend the book Java Concurrency in Practice .
Of course, the implementation of Java , from an engineering point of view, is well designed, but it is difficult to use in everyday work and it is quite verbose. In addition, there are not so many implementations for non-blocking programming in Java . You can often catch yourself by starting a stream, you completely forget that you quickly find yourself in a blocking code (on locks, expectations, etc.). An alternative non-blocking approach is difficult to use in everyday work, and it is easy to make mistakes in it.
Qorutinas, on the other hand, look like simple sequential code, hiding all the complexity inside libraries. At the same time, they provide the ability to run asynchronous code without any locks, which opens up great opportunities for various applications. Instead of blocking threads, calculations become interrupted. JetBrains describe cortinos as “lightweight threads,” of course, not the Threads that we know in Java . Korutin are very cheap to create, and the overhead in comparison with the threads do not go to any comparison. As you will see later, cortinas are launched in Threads under library management. Another significant difference is limitations. The number of threads is limited, since they actually correspond to native threads. Creating korutiny, on the other hand, is practically free, and even thousands of them can be easily launched.
There are many different approaches to multithreaded programming in various languages: based on callback
(JavaScript), future/promise
(Java, JavaScript), async/await
approach ( #), etc. All of them can be implemented with the help of Corutin due to the fact that they do not impose a programming style. On the contrary, any style is either already implemented or can be implemented with their help.
As one of the advantages, compared to the callback
based approach, corutines allow you to implement asynchronous code that will look like a serial one. And even though coroutines can run in multiple threads, your code will still look consistent and therefore easy to understand.
This is not to say that the concept of “coroutine” is new. According to an article from Wikipedia , the name itself was already known in 1958. Many modern programming languages ​​provide native support: C # , Go , Python , Ruby , etc. The implementation of Korutin, including in Kotlin , is often based on the so-called “ Continuations ”, which are an abstract representation of a controlled state in computer programs. We will return to how they work (implementation of korutin) .
There is an exhaustive material available on the site kotlinlang.org , which is well described how to set up a project to work with coroutines. Look in more detail at the material from the previous link, or simply take as a basis the code from my repository on GitHub .
As already mentioned, the library with corortes provides a clear high-level API that allows us to quickly get started. The only modifier you need to learn is to suspend. It is used as an additional modifier in methods to mark them as interruptible.
A little later, we will look at a few simple examples from the API , but for now, to begin with, let's take a closer look at what the function suspend
.
Korutiny based on the keyword suspend
, which is used to show that the function can be interrupted. In other words, the call to such a function can be interrupted at any time. Such functions can only be called from Corutin, which, in turn, requires at least one running function.
suspend fun myMethod(p: String): Boolean { //... }
As can be seen from the example above, the function with interruption looks like a normal function with an additional modifier. Keep in mind that such functions can only be called from Corutin, otherwise it will lead to compilation errors.
The quotes can be either a sequence of normal functions or functions with an interrupt with an optional result that will be available after execution.
After all the blah blah blah let's go over to specific examples. Let's start with the basics:
fun main(args: Array<String>) = runBlocking { //(1) val job = launch(CommonPool) { //(2) val result = suspendingFunction() //(3) println("$result") } println("The result: ") job.join() //(4) } >> prints "The result: 5"
In this example, two functions, (1) runBlocking
and (2) launch
, are examples of the use of Coruntine builders. There are a large number of different builders, each of which creates a corutin for different purposes: launch
(create and forget), async
(return promise
), runBlocking
(block the stream), and so on.
The internal cortina created with (2) launch
does all the work. The call of the interrupted function (3) can be interrupted at any time, the result will be output after the calculation. In the main thread after the start of the corutin, the String
value will be displayed before the corutin is completed. Korutina, launched via launch
, returns immediately a Job
, which can be used to cancel execution or to wait for calculations using (4) join()
. And since the call to join()
can be interrupted, we need to wrap it in another corute, which is often used in runBlocking
. This builder (1) was specially created to provide the ability for code written in the style of interrupts to be called in the usual blocking form ”(Note from the API ). If, however, remove join
(4), the program will end before the quorutine displays the value of the result.
Consider an example more close to reality. Imagine that in the application you need to send an email. Request recipients and generate text messages take considerable time. Both processes are independent, and accordingly we can execute them in parallel.
suspend fun sendEmail(r: String, msg: String): Boolean { //(6) delay(2000) println("Sent '$msg' to $r") return true } suspend fun getReceiverAddressFromDatabase(): String { //(4) delay(1000) return "coroutine@kotlin.org" } suspend fun sendEmailSuspending(): Boolean { val msg = async(CommonPool) { //(3) delay(500) "The message content" } val recipient = async(CommonPool) { //(5) getReceiverAddressFromDatabase() } println("Waiting for email data") val sendStatus = async(CommonPool) { sendEmail(recipient.await(), msg.await()) //(7) } return sendStatus.await() //(8) } fun main(args: Array<String>) = runBlocking(CommonPool) { //(1) val job = launch(CommonPool) { sendEmailSuspending() //(2) println("Email sent successfully.") } job.join() //(9) println("Finished") }
You can slightly simplify the author's code
suspend fun sendEmailSuspending(): Boolean { val msg = async(CommonPool) { delay(500) "The message content" } val recipient = async(CommonPool) { getReceiverAddressFromDatabase() } println("Waiting for email data") return sendEmail(recipient.await(), msg.await()) } fun main(args: Array<String>) = runBlocking(CommonPool) { sendEmailSuspending() println("Email sent successfully.") println("Finished") }
First, as we have seen in the previous example, we use (1) launch
builder inside runBlocking
, so we can (9) wait for the execution of the coroutine. In (2), we call the sendEmailSuspending
interrupted function. Inside this method, we use two parallel tasks: (3) to get the message text and (4) to call getReceiverAddressFromDatabase
to get the address. Both tasks are performed in parallel korutinah using async
. It is also worth noting that the call to delay is not blocking, it is used to interrupt the execution of cortina.
This builder is really simple in its concept. In other languages, he would return a promise , which, strictly speaking, is represented in Kotlin as a type Deferred
. All of these entities as promise
, future
, deferred
or delay
are usually interchangeable and are descriptions of the same. An asynchronous object that “promises” to return the result of the calculation and which can be waited at any time.
We have already seen the “waiting” part of the Deferred
objects from Kotlin in (7), where the interrupted function was called with the results of waiting for both methods. The await()
method is called on an instance of a Deferred object whose call is interrupted until the result is available. The call to sendEmail
also wrapped in an asynchronous builder so that we can wait for execution.
An important part of the examples above is the first parameter passed to the build function, which is an instance of the CoroutineContext
class. This context is what we pass into coruntine and what provides access to our current Job
.
The current context can be used to start the internal quorutine, and as a result, the child Job
will be a descendant of the external. This, in turn, allows you to override the entire hierarchy of quorutin with a single call on the parent Job
.CoroutineContext
contains various Element
types, one of which is CoroutineDispather
.
In all the examples above, CommonPool
used, which is the very dispatcher
. He is responsible for ensuring that the cortinas are executed in a thread pool under the control of the framework. Alternatively, we can use either a limited flow, either specially created, or using our own pool. The context can be easily combined using the + operator:
launch(CommonPool + CoroutineName("mycoroutine")){...}
You, probably, have already thought: corortines look, of course, good, but how will we perform synchronization and how will we exchange data between different coroutines?
Well, this is exactly the question I was asked recently, and this is a reasonable question for most Corutin using a pool of threads. For synchronization, we can use different techniques: thread-safe data structures, restriction on execution in one thread, or use locks (see Mutex
for Mutex
)
In addition to common patterns, Kotlin cortuettes encourage us to use the “exchange through communication” style (see QA ).
In particular, the actor is well suited for communication. It can be used in quotes that can send / receive messages from it. Let's look at an example:
sealed class CounterMsg { object IncCounter : CounterMsg() // one-way message to increment counter class GetCounter(val response: SendChannel<Int>) : CounterMsg() // a request with channel for reply. } fun counterActor() = actor<CounterMsg>(CommonPool) { //(1) var counter = 0 //(9) actor state, not shared for (msg in channel) { // handle incoming messages when (msg) { is CounterMsg.IncCounter -> counter++ //(4) is CounterMsg.GetCounter -> msg.response.send(counter) //(3) } } } suspend fun getCurrentCount(counter: ActorJob<CounterMsg>): Int { //(8) val response = Channel<Int>() //(2) counter.send(CounterMsg.GetCounter(response)) val receive = response.receive() println("Counter = $receive") return receive } fun main(args: Array<String>) = runBlocking<Unit> { val counter = counterActor() launch(CommonPool) { //(5) while(getCurrentCount(counter) < 100){ delay(100) println("sending IncCounter message") counter.send(CounterMsg.IncCounter) //(7) } } launch(CommonPool) { //(6) while ( getCurrentCount(counter) < 100) { delay(200) } }.join() counter.close() // shutdown the actor }
In the example above, we used Actor
, which is coruntine by itself and can be used in any context. Actor contains the current state of the application, which is contained in counter
. Here we also meet another interesting functionality (2) Channel
Channels provide us with the ability to exchange a stream of values, which is very similar to how we use BlockingQueue
(implementing the producer-consumer pattern) in Java , only without blocking. In addition, send
and receive
are interrupted functions and are used to receive and send messages through a channel that implements the FIFO strategy.
The actor by default contains such a channel and can be used in other quotes to send messages to it. In the example above, the actor iterates over messages from its own channel ( for
works here with interrupted calls), processing them according to their type: message (4) IncCounter
increases the counter
value in the actor, while message (3) GetCounter
causes the actor to return the value of counter in the form of sending an independent value to the SendChannel
channel from GetCounter
.
The first korutin in method (5) main
just for convenience, it sends the message (7) IncCounter
to the actor as long as the counter value is less than 100. The second (6) waits for the counter value to be less than 100. Both coroutines use an interrupted function (8) getCurrentCounter
, to which GetCounter
messages are GetCounter
and interrupted while waiting for a receive
to be returned.
As we see, the entire state is isolated in a particular actor. This solves the problem of the overall changeable state.
If you want to dive deeper into Korutiny and work with them, then I recommend reading the Kotlin documentation in more detail and, in particular, see the excellent guide .
I will not dive too deep into the details so as not to overload the post. In addition, in the next few weeks I plan to write a sequel with more detailed information on the implementation, along with examples of bytecode generation. Therefore, we now confine ourselves to a simple description “on the fingers”.
Korutin are not based on JVM functionality or operating system functionality. Instead, the cortices and interrupted functions are converted by the compiler into a state machine that can interrupt interrupts and send them to interrupted functions while maintaining the state. All this is possible thanks to the Continuation
, which is added by the compiler, as an additional implicit parameter, to each call of the function being interrupted. This is the so-called Continuation-passing style. More detailed description can be found here .
Not long ago, I was able to talk with Roman Elizarov from JetBrains , thanks to which in many ways Korutins appeared in Kotlin . Let me share the information with you:
Q: The first question that arises in me is: when should I use coroutines and are there any situations where it will still be necessary to use streams?
A: Korutiny needed for asynchronous tasks that expect something most of the time. Threads for intensive CPU tasks.
Q: I mentioned that the phrase “lightweight threads” sounds a bit deceptive to me, especially considering that the corutines are based on threads and run in the thread pool. It seems to me that the Korutinians are more like “task”, which is executed, interrupted, stopped.
A: The phrase “lightweight streams” is rather superficial, korutiny largely behave like streams from the point of view of users.
Q: I would like to know about synchronization. If the corutines are in many ways similar to the flows, then it will be necessary to implement synchronization of the general state between the different corutins.
A: You can use known patterns for synchronization, but it’s still preferable not to have a common state at all when using quorutin. Instead, the korutinas “encourage a style of communication through communication.”
Korutiny is a very powerful feature that appeared in Kotlin . Until I met Corutinos, it seemed to me that multithreading from Java was enough.
In contrast to Java , Kotlin presents a completely different style of competitive programming, not blocking in nature, which does not force us to run a huge number of native threads. In Java, it is quite normal to create another additional thread or a new pool, without thinking that this is a huge overhead and this can slow down the application in the future. Alternatively, korutins are the so-called “lightweight streams”, emphasizing that they do not correlate one to one in native streams and are not subject to such problems as deadlocks
, starvation
, etc. As we have already seen, you can not worry about blocking threads, synchronization with corints, they look more straightforward, especially if we adhere to “communication through communication”
Korutin also allow us to use different approaches to write competitive code, each of which is either already implemented in the library ( kotlinx.coroutine ), or can be easily implemented with its help.
Java developers are probably more accustomed to sending tasks to the thread pool and waiting for the future
result using ExecutorService
, which, as we have seen, is easily implemented using async/await
. Yes, this is not a fully equivalent replacement, but it is still a significant improvement.
Redefine your approach to competitive programming in Java , all of these checked exceptions, hard-blocking strategies, and a huge amount of generic code. It is quite normal to write code sequentially with corortines using suspend
function calls, communicating with other corutines, waiting for the result, canceling corutines, etc.
Nevertheless, I am convinced that the Corutins are truly incredible. Of course, only time will tell if they are really mature for high-load multi-threaded applications. Maybe even many programmers will think and reconsider their approaches to programming. Curious to see what will happen next. , , , JetBrains , , .
Fine! . , - . .
Simon
Source: https://habr.com/ru/post/336228/
All Articles