kotlin coroutine understanding
Reprint address: https://www.jianshu.com/p/9f720b9ccdea
This article mainly introduces the usage of coroutines and the benefits of using coroutines. In addition, it will also briefly mention the general principles of coroutines.
The meaning of this article may just let you know about coroutines , and willing to start using it.
If you want to thoroughly understand coroutines, please check the official documentation, the official documentation link will be given at the end of the article.
If you have learned coroutines in other languages before, such as Python’s yield, please forget them first. After all, there are still some differences. After you understand Kotlin’s coroutines, compare them. Otherwise, There may be some preconceived ideas that hinder your understanding, and I have suffered from this.
First knowledge of coroutines:
First of all, let’s take a look at what a coroutine looks like. Here is an example from the official website:
fun main(args: Array) {
launch(CommonPool) {
delay(1000L)
println("World!")
}
println("Hello,")
Thread. sleep(2000L)
}
/*Running result: ("Hello," will be printed immediately, after 1000 milliseconds, "World!" will be printed)
Hello,
World!
*/
Leaving aside the specific details, the general operation process of the above code is as follows:
A. Main process:
- Calling the launch method of the system starts a coroutine, and the following braces can be regarded as coroutine body.
(where CommonPool is temporarily understood as a thread pool, which specifies where the coroutine runs) - Print out “Hello,”
- The main thread sleeps for two seconds
(the sleep here is just to keep The process survives, the purpose is to wait for the execution of the coroutine)
B. Coroutine process:
- The coroutine delays for 1 second
- Print out “World!”
Explain the delay method:
In the coroutine, the delay method is equivalent to the sleep in the thread, which is to rest for a period of time, but the difference is that the delay will not block the current thread, but like setting an alarm clock , before the alarm goes off, the thread running the coroutine can be scheduled to do other things, and when the alarm goes off, the coroutine will resume running.
It can also be canceled after the coroutine is started
The launch method has a return value, the type is Job, and the Job has a cancel
method, which can be canceled by calling the cancel method Coroutine, see an example of counting sheep:
fun main(args: Array) {
val job = launch(CommonPool) {
var i = 1
while(true) {
println("$i little sheep")
++idelay(500L) // count one every half second, you can lose two in one second
}
}
Thread.sleep(1000L) // During the sleep of the main thread, two sheep have been counted in the coroutine
job.cancel() // The coroutine only counted two sheep, and it was canceled
Thread. sleep(1000L)
println("main process finished.")
}
The result of the operation is:
1 little sheep
2 little sheep
main process finished.
If you don’t call cancel, you can count up to 4 sheep.
The core of the coroutine is the suspend method. Let’s explain the suspend method first, and then continue with other topics.
Understand the suspend method:
The suspend method is the core of the coroutine, and understanding the suspend method is the key to using and understanding the coroutine.
(suspend lambda is similar to the suspend method, but it has no name, so it will not be introduced separately)
The syntax of the suspend method is very simple, but there is only one more suspend
keyword than the ordinary method:
suspend fun foo(): ReturnType {
//...
}
The suspend method can only be called inside the coroutine, not outside the coroutine.
In essence, the suspend method is quite different from ordinary methods. The essence of the suspend method is to return asynchronously(Note: not an asynchronous callback). We will explain the meaning of this sentence later.
Now, let’s look at an example of an asynchronous callback:
fun main(...) {
requestDataAsync {
println("data is $it")
}
Thead.sleep(10000L) // This sleep is just to keep the process alive
}
fun requestDataAsync(callback: (String)->Unit) {
Thread() {
// do something need lots of times.
//...
callback(data)
}.start()
}
The logic is very simple, that is to pull a piece of data through an asynchronous method, and then use this data. According to the previous programming method, if you want to receive the data returned asynchronously, you can only use callback.
But if you use coroutines, Instead of using the callback, the data can be “returned” directly. The caller does not use the callback to accept the data, but accepts the return value like calling a synchronous method. If the above function is changed to a coroutine, it will be:
fun main(...) {
launch(Unconfined) { // Please focus on how to obtain asynchronous data in the coroutine
val data = requestDataAsync() // The data returned asynchronously is returned like synchronously
println("data is $it")
}
Thead.sleep(10000L) // Please don't pay attention to this sleep
}
suspend fun requestDataAsync() { // Please note that there is an additional suspend keyword before the method
return async(CommonPool) { // Don't worry about this async method, explain later
// do something need lots of times.
//...
data // return data, return in lambda should be omitted
}. await()
}
Here, we first turn requestDataAsync into a suspend method, and the change of its prototype is:
- Added a
suspend
keyword before. - Removed the original callback parameter.
Let’s not delve into the new implementation of this method here, and I will explain it later.
The points that need to be paid attention to here are: In the coroutine, call the suspend method, and the asynchronous data will be returned just like synchronously .
How does this work?
When the program executes inside requestDataAsync, another new sub-coroutine is started through async to pull data, and this new After the child coroutine, the current parent coroutine is suspended, and requestDataAsync has not returned at this time.
The child coroutine has been running in the background, and after a while, the child coroutine will resume after pulling the data back Its parent coroutine, the parent coroutine continues to execute, and requestDataAsync returns the data.
In order to deepen the understanding, let’s compare another example: without using coroutines, the asynchronous method can also be converted into a synchronous method (in unit tests, we often do this):
fun main(...) {
val data = async2Sync() // The data is returned synchronously, but the thread is also blocked
println("data is $it")
// Thead.sleep(10000L) // This sentence is meaningless here, comment it out
}
private var data = ""
private fun async2Sync(): String {
val obj = Object() // just create an object to use as a lock
requestDataAsync { data ->
this.data = data // temporarily store data
synchronized(locker) {
obj. notifyAll() // Notify all waiters
}
}
obj.wait() // blocking wait
return this.data
}
fun requestDataAsync(callback: (String)->Unit) {
// ...normal async method
}
Note that compared to the previous coroutine example, this is superficially the same as it, but here the main method will block and wait for the async2Sync() method to complete. The same is waiting, the coroutine will not block the current thread, but will actively give up the execution right, which is equivalent to dismissing the current thread and letting it do other things.
In order to better understand the meaning of this “dismissal”, let’s look at another example:
fun main(args: Array) {
// 1. Program start
println("${Thread. currentThread(). name}: 1");
// 2. Start a coroutine and start it immediately
launch(Unconfined) { // Unconfined means to run the coroutine in the current thread (main thread)
// 3. This coroutine starts executing the first step directly on the main thread
println("${Thread. currentThread(). name}: 2");
/* 4. The second step of this coroutine calls a suspend method, after calling,
* This coroutine will give up the execution right, dismiss the thread (main thread) that runs me, please do something else.
*
* When delay is called, a timer is created internally and a callback is set.
* When the timer expires after 1 second, the callback just set will be called.
* In the callback, the interface of the system will be called to restore the coroutine.* The coroutine resumes execution on the timer thread. (Not the main thread, related to Unconfined)
*/
delay(1000L) // After 1 second, the timer thread will resume the coroutine
// 7. The timer thread resumes the coroutine,
println("${Thread. currentThread(). name}: 4")
}
// 5. The coroutine just now doesn't want me (the main thread) to work, so I continue the previous execution
println("${Thread. currentThread(). name}: 3");
// 6. I (the main thread) sleep for 2 seconds
Thread. sleep(2000L)
// 8. I (the main thread) continue to execute after sleeping
println("${Thread. currentThread(). name}: 5");
}
Operation result:
main: 1
main: 2
main: 3
kotlinx.coroutines.ScheduledExecutor: 4
main: 5
The comments of the above code list the program operation process in detail. After reading it, you should be able to understand the meaning of “dismissal” and “abandonment of execution rights”.
Unconfined
means not specifying the running thread for the coroutine, and using whoever it catches, and the thread that starts it executes it directly, but after being suspended, it will be resumed by the thread Continue to execute. If a coroutine is suspended multiple times, it may be resumed by a different thread each time it is resumed.
Now let’s review the sentence just now: The essence of the suspend method is to return asynchronously.
The meaning is to split it into “asynchronous” + “return”:
- First of all, the data is not returned synchronously (synchronous refers to immediate return), but is returned asynchronously.
- Secondly, accepting data does not need to pass callback, but directly receive the return value.
The detailed process of calling the suspend method is:
In the coroutine, if a suspend method is called, the coroutine will be suspended and its execution right will be released, but before the coroutine is suspended, the suspend method Generally, another thread or coroutine is started internally, let’s call it “branch execution flow” for the time being, its purpose is to calculate and obtain a data.
When the *branch execution flow” in the suspend method is completed, It will call the system API to resume the execution of the coroutine, and return the data to the coroutine (if any).
__Why can’t the suspend method be called outside the coroutine?__
The suspend method can only be called inside the coroutine, the reason is that the current thread can be dismissed only in the coroutine, and it is not allowed outside the coroutine Dismissal, think in reverse, what if threads can be disbanded outside the coroutine, write a counterexample:
fun main(args: Array) {
requestDataSuspend();
doSomethingNormal();
}
suspend fun requestDataSuspend() {
//...
}
fun doSomethingNormal() {
//...
}
requestDataSuspend is a suspend method, and doSomethingNormal is a normal method. DoSomethingNormal must wait until requestDataSuspend is executed before it starts. As a result, the main method loses the ability to parallelize, and all places lose the ability to parallelize. This is definitely not what we want, so we need It is agreed that the thread can only be dismissed in the coroutine and the right to execute is given up, so the suspend method can only be called in the coroutine.
Concept Explanation: Continuation and suspension point
——Personally, I suggest that proper nouns should not be translated into Chinese, otherwise it will be easy becauseSentence errors and misunderstandings
The execution of the coroutine is actually intermittent: execute a period, suspend it, execute another period, suspend it again, …
Each suspension point is a suspension point
, every A small section of execution is a Continuation
.
The execution flow of the coroutine is divided into many “Continuations” by its “suspension point”.
We can draw many with one A line segment of a point to represent:
Execution Flow Segmentation for Coroutines
The Continuation 0 is special, it starts from the starting point and ends at the first suspension point. Because of its particularity, it is also called Initial Continuation
.
After the coroutine is created, it is not always executed immediately. To distinguish how to create the coroutine, the second parameter of the launch method is an enumeration type
CoroutineStart
, if not filled, The default value isDEFAULT
, then the long-term coroutine will start immediately after creation. IfLAZY
is passed in, it will not start immediately after creation untilstart method will start.
Suspension point is just a concept, and Continuation has a corresponding interface in Kotlin, which will be introduced later.
Encapsulate asynchronous callback method
In a world without coroutines, usually asynchronous methods need to accept a callback to publish the result of the operation.
In coroutines, all methods that accept cThe callback method can be converted into a suspend method that does not require callback.
The requestDataSuspend method above is an example of this, let's take a look back:
suspend fun requestDataSuspend() {
return async(CommonPool) {
// do something need lots of times.
//...
data // return data
}. await()
}
It is implemented internally by calling the async and await methods (we will introduce async and await later), so although there is no problem in realizing the function, it is not the most suitable way. The above is just for the shortest implementation , a reasonable implementation should be to call the suspendCoroutine
method, probably like this:
suspend fun requestDataSuspend() {
suspendCoroutine { cont ->
// ... details omitted for now
}
}
// can be abbreviated as:
suspend fun requestDataSuspend() = suspendCoroutine { cont ->
//...
}
Before the complete implementation, you need to understand the suspendCoroutine
method, which is a method in the Kotlin standard library. The prototype is as follows:
suspend fun suspendCoroutine(block: (Continuation) -> Unit): T
When we discuss the official Kotlin coroutine API later, we will know that there are very few low-level APIs in the Kotlin standard library to support coroutines (most APIs are not in the standard library, but in the extension library of the application layer, such as The launch method above), this is one of them.
The function of suspendCoroutine is to suspend the current execution flow, and resume the execution of the coroutine at the right time. We can see that its parameter is a lambda, and the lambda's The parameter is a Continuation
. We have actually mentioned Continuation just now. It represents a section of execution flow. I won’t explain too much here. The Continuation instance in this method The representative execution flow starts from the current suspension point and ends at the next suspension point. The current suspension point is the moment when suspendCoroutine is called.
After calling suspendCoroutine, the current execution flow will be suspended (the thread calling suspendCoroutine will be Dismissal, but not the whole process is suspended, otherwise who will do the work), and then open another execution flow to do asynchronous things, wait until the asynchronous things are done, the current execution flow will resume, let's see how to restore .
suspendCoroutine will automatically capture the current execution environment (such as temporary variables, parameters, etc.), then store it in a Continuation, and pass it to its lambda as a parameter.
It has been mentioned before that Continuation is the standard An interface in the library, its prototype is:
interface Continuation {
val context: CoroutineContext // ignore this for now
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
It has two methods resume
and resumeWithException
:
- If calledresume is normal recovery
- calling resumeWithException is abnormal recovery
Now let's improve the example just now:
suspend fun requestDataSuspend() = suspendCoroutine { cont ->
requestDataFromServer { data -> // Ordinary method still accepts data through callback
if (data != null) {
cont. resume(data)
} else {
cont. resumeWithException(MyException())
}
}
}
/** Ordinary asynchronous callback method */
fun requestDataFromServer(callback: (String)->Unit) {
// ... get data from server, it will call back when finished.
}
The logic is very simple, if the data is valid, it will resume normally, otherwise it will resume abnormally.
But here it should be noted that: The parameter passed to resume will become the return value of suspendCoroutine, and then It has become the return value of the requestDataSuspend method.
This place is amazing, how does Kotlin do it? It is estimated that it will be difficult to understand in a short time, so remember it first.
suspendCoroutine has a feature:
suspendCoroutine { cont ->
// If this lambda returns, neither resume nor resumeWithException of cont is called
// Then the current execution flow will be suspended, and the time of suspension is before suspendCoroutine
// At onceIt hangs before the internal return of suspendCoroutine
// If the resume or resumeWithException of cont is called before returning in this lambda
// Then the current execution flow will not be suspended, suspendCoroutine returns directly,
// If resume is called, suspendCoroutine will return a value like a normal method
// If resumeWithException is called, suspendCoroutine will throw an exception
// This exception can be caught outside by try-catch
}
Looking back, did the implementation just call the resume method, let's fold it:
suspend fun requestDataSuspend() = suspendCoroutine { cont ->
requestDataFromServer { ... }
}
It’s clear, there is no call, so suspendCoroutine hangs before returning, but before the suspension, the lambda is executed, the requestDataFromServer is called in the lambda, and the real process of doing things (asynchronous execution) is started in the requestDataFromServer, and suspendCoroutine is hanging and waiting.
When requestDataFromServer finishes its work, the incoming callback will be called, and cont.resume(data) is called in this callback, the outer coroutine will resume, and then suspendCoroutine will Return, the return value is data.
You must be very curious about how Kotlin is implemented internally. To fully understand the mystery, you still need to read the official documents and codes. Here is just a brief introduction to the general principle. I don’t understand the details. It doesn’t matter if you don’t understand it.
Inside Kotlin, the coroutine is implemented as a state machine, the number of states is the number of suspension points + 1 (initial state), the current stateThe state is the current suspension point. When resume is called, the next Continuation will be executed.
It is estimated that everyone should have a vague understanding at this time. In fact, as a user, this is enough, but if you want to study in depth, you still need to study the code by yourself.
async/await mode:
We have used the launch method many times before, its function is to create a coroutine and start it immediately, but there is a problem, that is, the coroutine created by the launch method cannot carry the return value. Async has also appeared before, but There has been no detailed introduction.
The function of the async method is basically the same as that of the launch method. It creates a coroutine and starts it immediately, but the coroutine created by async can carry a return value.
The return value type of the launch method isJob, the return value type of the async method is Deferred, which is a subclass of Job, and there is a <code in Deferred >await method, call it to get the return value of the coroutine.
async/await
is a commonly used mode. The meaning of async is to start an asynchronous operation, and the meaning of await is to wait for the result of this asynchronous operation.
Who wants to wait for it, In the traditional code that does not use coroutines, the thread is waiting (the thread is not doing anything else, just waiting there). In the coroutine, it is not the thread that is waiting, but the execution flow that is waiting strong>, the current process is suspended (the underlying thread will be dismissed to do other things), and the process will not continue to run until the calculation result is available.
So we can draw another conclusion by the way: The execution flow in the coroutine is linear, whether the steps are synchronous or asynchronous, the subsequent steps will wait for the previous steps to complete.
We can start multiple tasks through async, and they will Running at the same time, the async posture we used before is not very normal, let’s take a look at the normal posture of using async:
fun main(...) {
launch(Unconfined) {
// Task 1 will be started immediately, and will be executed in parallel on other threads
val deferred1 = async { requestDataAsync1() }
// The last step only starts task 1, and does not suspend the current coroutine
// So task 2 will also start immediately, and will be executed in parallel on other threads
val deferred2 = async { requestDataAsync2() }
// Wait for task 1 to end first (waited for about 1000ms),
// Then wait for task 2, since it was started almost at the same time as task 1, it is also completed quickly
println("data1=$deferred2. await(), data2=$deferred2. await()")
}
Thead.sleep(10000L) // continue to ignore this sleep
}
suspend fun requestDataAsync1(): String {
delay(1000L)
return "data1"
}
suspend fun requestDataAsync2(): String {
delay(1000L)
return "data2"
}
The running result is very simple, needless to say, but what is the total time-consuming of the coroutine, about 1000ms, not 2000ms, because the two tasks are running in parallel.
There is a problem: if task 2 precedes Task 1 is completed, what is the result?
The answer is: the result of task 2 will be saved in deferred2 first, and when deferred2.await() is called, it will return immediately and will not cause the coroutine to hang, because deferred2 is ready.
So, suspend method does not always cause the coroutine to suspend, only when its internal data is not ready.
It should be noted that await is a suspend method, but async is not, so it can be called outside the coroutine, async only starts the coroutine, async itself will not cause the coroutine to hang, and the lambd passed to asynca (that is, the coroutine body) may cause the coroutine to hang.
async/await In other languages, the pattern is implemented as two keywords, but in Kotlin it is just two very common methods.
Generators introduction:
When learning Python coroutines, the first thing to learn is Generators. Its function is to generate sequences through calculations instead of storage mechanisms such as lists. The following uses Generators to generate Fibonacci sequences:
// inferred type is Sequence
val fibonacci = buildSequence {
yield(1) // first Fibonacci number
var cur = 1
var next = 1
while (true) {
yield(next) // next Fibonacci number
val tmp = cur + next
cur = next
next = tmp
}
}
fun main(...) {
launch(Unconfined) { // Please focus on how to obtain asynchronous data in the coroutine
fibonacci.take(10).forEach { print("$it, ") }
}
Thead.sleep(10000L) // Please don't pay attention to this sleep
}
// will print 1, 1, 2, 3, 5, 8, 13, 21, 34, 55,
I think there is nothing to explain this, yield
is a suspend method, which gives up the right to execute and returns the data.
According to the previous knowledge, we can infer that the yield internally must will eventually call into CThe resume method of ontinuation.
yield is generally a keyword in other languages, and it is also a method in Kotlin.
yield is an API in the standard library. In most cases, we don’t need to call this method directly. It is more convenient to use the Channel
and produce
methods in kotlinx.coroutines. Specifically You can refer to here:
https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#building-channel-producers
The sequence generation is actually a bit like RX, But it is also different, for details, please refer to here:
https://github.com/Kotlin/kotlinx.coroutines/blob/master/reactive/coroutines-guide-reactive.md
There is currently no scenario that specifically requires the use of Generators, so there is not much discussion here.
Coroutine API Description:
Kotlin developers have a unique implementation of coroutines. The language mechanism itself only adds very few keywords, and the standard library has only very few APIs, but this does not mean that there are few functions. According to the design of Kotlin, many Functional APIs are implemented in higher-level application libraries.
Only a small number of core mechanisms are placed in the language itself and the standard library. This not only makes the language simpler, but also more flexible.
Kotlin officially provides three levels of capability support for coroutines, namely: the lowest language layer, the middle layer standard library(kotlin-stdlib), And the top application layer(kotlinx.coroutines).
Application layer:
This layer is directly called by our program, providing some common implementation methods, such as launch method, async method, etc., its implementation is in kotlinx.coroutines.
Standard library:
The standard library only provides a small number of methods for creating coroutines, located at:
kotlin.coroutines.experimental:
– createCoroutine()
– startCoroutine()
– suspendCoroutine()
So far, we have only used the suspendCoroutine
method directly.
In the implementation of launch and async methods, the startCoroutine
method is finally called.
The buildSequence method in Generators will eventually call buildSequence
to achieve.
Language layer:
The language itself mainly provides support for the suspend
keyword, and the Kotlin compiler will treat suspend-modified methods or lambdas specially, Generate some intermediate classes and logic code.
What we usually use is basically the interface of the application layer. The application layer provides many very core functions. Most of these functions are realized by keywords in other languages, but in Kotlin, these are implemented as method.
Summary
What is a coroutine:
After reading so many examples, we can now summarize what a coroutine is, and what exactly is a coroutine. It is difficult to give a specific definition. Even if a specific definition can be given, it will be very abstract and difficult to understand.
On the other hand, the coroutine can be said to be the ability of the compiler, because the coroutine does not need the support of the operating system and hardware (threads), it is the compiler that provides some key functions for developers to write code more easily and conveniently. word, and automatically generates some supporting code (probably bytecode) internally.
The following is my personal summary:
- First of all, a coroutine is a block of code that contains specific logic, and this code block can be scheduled to be executed on different threads;
- Secondly, a coroutine is an environment in which In, the method can be awaited to execute, with the operation resultReturn later, during the waiting period, the thread resources carrying the coroutine can be used elsewhere.
- Third, the coroutine is a logical process independent of the running process. The steps in the coroutine, regardless of Whether it is synchronous or asynchronous, it is linear (completed from front to back).
Difference and relationship between coroutine and thread:
Threads and coroutines have fundamentally different purposes:
- The purpose of the thread is to improve the utilization rate of CPU resources, so that multiple tasks can run in parallel, so the thread is to serve the machine.
- The purpose of the coroutine is to allow multiple Better collaboration between tasks is mainly reflected in the code logic, so the coroutine is to serve people, the people who write the code. (It is also possible that the result can improve the utilization of resources, but it is not the original purpose)
In terms of scheduling, coroutines are also different from threads:
- The scheduling of threads is completed by the system, generally preemptive, allocated according to priority, and is space division multiplexing.
- The scheduling of coroutines is specified by the developer according to the program logic Well, the reasonable allocation of resources to different tasks in different periods is time-division multiplexed.
The difference in function:
- The coroutine ensures that the code logic is sequential, no matter if the synchronous operation is an asynchronous operation, the previous one is completed, and the latter one will start.
- Threads can be scheduled to execute on the CPU, so that The code can really run.
Relationship between coroutines and threads:
Coroutines do not replace threads, but are abstracted from threads. Threads are divided CPU resources, and coroutines are well-organized codes Process, coroutines need threads to run. Threads are the resources of coroutines, but coroutines will not directly use threads. Coroutines directly use Executor (Interceptor), and the executor can be associated with any Thread or thread pool, can use current thread, UI thread, or create a new process. It can be summarized as follows:
- Threads are the resources of the coroutine.
- The coroutine uses the resource of the thread indirectly through the Interceptor.
Conclusion:
If you need to use coroutines frequently, it is recommended to take the time to read the official documents.
Finally, thank you for reading, I hope this article is helpful to you!
Official English document link:
- Introduction to the official coroutine
- The full version of the user guide
- Detailed explanation of the current implementation
_
The first page is a subpage of the official guide, the second and third are the markdown documents in the two GitHub projects respectively, and the projects they are in also contain other documents, if necessary Browse by yourself.
In addition, I would like to make a suggestion: If you are stuck, you can skip or consult other documents, and then come back to look at them later. Other documents may be used in other ways or in other ways example to describe the same thing)