Solution to Kotlin Coroutine (2) - Coroutine Startup
Now that you know what coroutines are all about, you might want to try them yourself. This article will give you a detailed introduction to the differences between the several startup modes of coroutines. Of course, I don’t plan to go deep into the principle of source code analysis now. You only need to remember these rules to use coroutines well.
1. Recall when you first learned Thread
I believe that the vast majority of developers who come into contact with Kotlin now have a Java foundation. When we first started learning Thread, we must have done this:
val thread = object : Thread(){
override fun run() {
super.run()
//do what you want to do.
}
}
thread.start()
Someone must have forgotten to call it start
, and I am particularly wondering why the thread I opened does not start. To be honest, the design start
of is actually very strange, but I understand the designers, after all, it was still stop
available back then, but they soon found that the design stop
was a mistake, and it was abandoned in JDK 1.1 because it was not safe, which is called The above is the most short-lived API.
Since
stop
it is a mistake,start
isn't it also a mistake that always makes beginners lose it?
Ha, I'm digressing a bit. We mainly talk about Kotlin today. The designers of Kotlin had a great idea, and they provided a convenient method for threads:
val myThread = thread {
//do what you want
}
This thread
method has a parameter start
that defaults to true
, in other words, the thread created in this way is started by default, unless you really don't want it to go to work right away:
val myThread = thread(start = false) {
//do what you want
}
//later on ...
myThread.start()
It looks much more natural this way. The interface design should allow the default value to meet 80% of the requirements.
2. Let's look at the start of the coroutine
Having said so many threads, the reason is, after all, everyone is most familiar with it. The API design of the coroutine is actually in the same line. Let's look at the simplest way to start the coroutine:
GlobalScope.launch {
//do what you want
}
So how does this code execute? As we said, three things are needed to start a coroutine, namely the context , the startup mode , and the coroutine body . The coroutine body is like the code Thread.run
in it , needless to say.
This article will introduce the startup . In Kotlin coroutines, the launch mode is an enumeration:
public enum class CoroutineStart {
DEFAULT,
LAZY,
@ExperimentalCoroutinesApi
ATOMIC,
@ExperimentalCoroutinesApi
UNDISPATCHED;
}
|
mode
|
Function
|
| — | — |
|
DEFAULT
|
Execute the coroutine body immediately
|
|
ATOMIC
|
Executes the body of the coroutine immediately, but cannot be canceled before it starts running
|
|
UNDISPATCHED
|
Immediately execute the body of the coroutine in the current thread until the first suspend call
|
|
LAZY
|
run only when needed
|
2.1 DEFAULT
Among the four startup modes, the most commonly used ones are actually DEFAULT
and LAZY
.
DEFAULT
It is a hungry Chinese style startup. launch
After calling, it will enter the waiting state immediately. Once the scheduler is OK, it can start to execute. Let's look at a simple example:
suspend fun main() {
log(1)
val job = GlobalScope.launch {
log(2)
}
log(3)
job.join()
log(4)
}
Note: The main function supports suspend starting from Kotlin 1.3. In addition, the omission of parameters in the main function is also a feature of Kotlin 1.3. The following examples are directly run in the suspend main function without special instructions.
This program uses the default startup mode. Since we did not specify a scheduler, the scheduler is also the default. On the JVM, the implementation of the default scheduler is similar to that of other languages. It has some threads in the background to handle asynchronous task, so the running result of the above program may be:
19:51:08:160 [main] 1
19:51:08:603 [main] 3
19:51:08:606 [DefaultDispatcher-worker-1] 2
19:51:08:624 [main] 4
It could also be:
20:19:06:367 [main] 1
20:19:06:541 [DefaultDispatcher-worker-1] 2
20:19:06:550 [main] 3
20:19:06:551 [main] 4
It depends on the CPU's scheduling order for the current thread and the background thread, but don't worry, you will soon find that the output order of 2 and 3 in this example is not that important.
You may have already guessed the implementation of the default scheduler on the JVM. Yes, a thread pool is opened, but a few threads are enough to schedule thousands of coroutines, and each coroutine has its own call stack. This is fundamentally different from purely opening a thread pool to perform asynchronous tasks. Of course, we say that Kotlin is a cross-platform language, so the above code can also run in a JavaScript environment, such as Nodejs. In Nodejs, the default scheduler of Kotlin coroutines does not implement thread switching, and the output results will be slightly different, which seems to be more in line with the execution logic of JavaScript. We will discuss more about the scheduler later.
2.2 LAZY
LAZY
It is a lazy startup, launch
and there will be no scheduling behavior afterwards, and the coroutine body will naturally not enter the execution state until we need it to execute. This is actually a bit confusing, what do we mean when we need it to execute? That is, when its operation result is needed, launch
an Job
instance . For this case, we can:
- Call
Job.start
, actively trigger the scheduled execution of the coroutine - call
Job.join
, which implicitly triggers the scheduled execution of the coroutine
So this so-called "need" is actually a very interesting wording, and you will see later that we can also use await
to express Deferred
the need for . This behavior Thread.join
differs , where calling join
has no effect if it is not started.
log(1)
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
log(2)
}
log(3)
job.start()
log(4)
Based on this, for the example above, the output might be:
14:56:28:374 [main] 1
14:56:28:493 [main] 3
14:56:28:511 [main] 4
14:56:28:516 [DefaultDispatcher-worker-1] 2
Of course, if you are lucky enough, there may also be a situation where 2 to 4 is ahead. And for join
,
...
log(3)
job.join()
log(4)
Because it is necessary to wait for the execution of the coroutine to complete, the output result must be:
14:47:45:963 [main] 1
14:47:46:054 [main] 3
14:47:46:069 [DefaultDispatcher-worker-1] 2
14:47:46:090 [main] 4
2.3 ATOMIC
ATOMIC
It only makes sense when cancel is involved. Cancel itself is also a topic worthy of detailed discussion. Here we simply think that the coroutine will be canceled after cancel, that is, it will no longer be executed. Then the timing of calling cancel is different, and the results are also different, such as before the coroutine is scheduled, the scheduling is started but not yet executed, the execution has already started, the execution is completed, and so on.
In order to understand the difference between it DEFAULT
and , let's look at an example:
log(1)
val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
log(2)
}
job.cancel()
log(3)
We cancel immediately after creating the coroutine, but because of ATOMIC
the mode , the coroutine will be scheduled, so 1, 2, and 3 will be output, but the order of 2 and 3 is hard to say.
20:42:42:783 [main] 1
20:42:42:879 [main] 3
20:42:42:879 [DefaultDispatcher-worker-1] 2
Correspondingly, if it is DEFAULT
the mode , if cancel has been called when the coroutine is scheduled for the first time, then the coroutine will be canceled directly without any call. Of course, it is also possible that the coroutine has not been canceled at the beginning, then it You can start normally. So if the previous example is changed DEFAULT
to mode , then 2 may or may not be output.
It should be noted that the cancel call will definitely set the status of the job to canceling, but the coroutine in ATOMIC
the mode ignores this status when it starts. To demonstrate this, we can make the example slightly more complex:
log(1)
val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
log(2)
delay(1000)
log(3)
}
job.cancel()
log(4)
job.join()
We add one between 2 and 3 delay
, delay
which will cause the execution of the coroutine body to be suspended, and the latter part will be scheduled again after 1000ms, so 3 will be output 1000ms after 2 is executed. For ATOMIC
the mode , we have already discussed that it must be started. In fact, its execution will not stop until it encounters the first suspend point, delay
but a suspend function. At this time, our coroutine ushers in its first suspend function. A suspension point happens delay
to support cancel, so the following 3 will not be printed.
When we use threads, we will face similar problems if we want to stop the execution of tasks in the thread, but unfortunately, the stop interface in the thread that looks similar to cancel has been abandoned because of some security issues. But as we continue to explore in depth, you will find that the cancel of the coroutine is more like the interrupt of the thread in a sense.
2.4 UNDISPATCHED
With the previous foundation, UNDISPATCHED
it is easy to understand. In this mode, the coroutine will directly start executing under the current thread until the first suspension point, which sounds a bit like the previous one, but the difference is that the ATOMIC
coroutine body is executed UNDISPATCHED
without any scheduler. Of course, the execution after encountering the suspension point depends on the logic of the suspension point itself and the scheduler in the context.
log(1)
val job = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
log(2)
delay(100)
log(3)
}
log(4)
job.join()
log(5)
We still use such an example to understand UNDISPATCHED
the mode . According to our previous discussion, the coroutine will be executed immediately in the current thread after it is started, so 1 and 2 will be executed continuously in the same thread, delay
which is the suspension point, so 3 will wait 100ms later Scheduling again, at this time 4 is executed, and join
it is required to wait for the coroutine to be executed, so wait for 3 to output before executing 5. The following are the results of the operation:
22:00:31:693 [main] 1
22:00:31:782 [main @coroutine#1] 2
22:00:31:800 [main] 4
22:00:31:914 [DefaultDispatcher-worker-1 @coroutine#1] 3
22:00:31:916 [DefaultDispatcher-worker-1 @coroutine#1] 5
The thread name is in the square brackets. We found that when the coroutine is executed, the thread name will be modified to make itself appear to have a sense of existence. The running results seem to have another detail that may be confusing. The
join
subsequent 5 threads are the same as 3. Why? We mentioned earlier that our examples all run in the suspend main function, so the suspend main function will help us directly start a coroutine, and the coroutines in our example are all its sub-coroutines, so the scheduling of 5 here depends on The scheduling rules of the outermost coroutine. We will talk about the scheduling of coroutines later.
3. Summary
This article uses some examples to gradually unveil the veil of coroutines.
Scan the QR code to get more Kotlin-related learning materials for free!
" Advanced Kotlin Enhanced Combat "
Chapter 1 Kotlin Getting Started Tutorial
● Kotlin overview
● Kotlin vs. Java
● Skillful use of Android Studio
● Understand the basic types of Kotlin
● Walk into Kotlin's array
● Walk into Kotlin collections
● Complete code
● Basic grammar
Chapter 2 Kotlin Practical Pit Avoidance Guide
● Method input parameters are constants and cannot be modified
● No Companion, INSTANCE?
● Java overloading, how to make a clever transition in Kotlin?
● Null gesture in Kotlin
● Kotlin overrides the method in the Java parent class
● Kotlin becomes "ruthless", even TODO is not spared!
● Pitfalls in is and as`
● Understanding of Property in Kotlin
● also keyword
● takeIf keyword
● How to write singleton mode
Chapter 3 Project Combat "Kotlin Jetpack Combat"
● Start with a demo that worships a great god
● What is the experience of writing Gradle scripts in Kotlin?
● The Triple Realm of Kotlin Programming
● Kotlin higher order functions
● Kotlin Generics
● Kotlin Extensions
● Kotlin delegates
● Coroutine "unknown" debugging skills
● Graphical coroutine: suspend