0 x0, introduction
In the previous section, The Boring Kotlin Coroutine Trilogy (PART 1) — Conceptual Enlightenment, I started by understanding the concepts related to concurrency and then led to the Kotlin coroutine:
True coroutines:
- A non-preemptive/cooperative task scheduling mode in which programs can actively suspend or resume execution;
- Thread-based, much lighter than threads, can be understood as the user layer to simulate thread operations;
- Context switching is controlled by the user to avoid the participation of a large number of interrupts and reduce the resources consumed by thread context switching and scheduling.
“Pseudocoroutine” in Kotlin
Instead of implementing a synchronization mechanism (locking) at the language level and relying on Java keywords provided by the Kotlin-JVM, the implementation of the locking is left to the threads, so the Kotlin coroutine is essentially a “wrapper based on the native Java Thread API.” It’s just that the API hides the asynchronous implementation details so that we can write asynchronous operations synchronously.
There is some debate about the classification of coroutines in Kotlin:
There is no allocation of function call stacks, and the start state is implemented by state machine or closure syntax, which is stackless coroutine without error.
However, it also shows the characteristics of stack coroutine: it can be suspended at any function call layer, and transfer the call weight.
These are the most important concepts. Before studying the specific API of Kotlin coroutine, LET me give you some useful ideas.
0x1. Thought work
① Context
How do you understand the word context? The resources needed to get something done. Here’s an example:
When you wake up in the morning on the weekend, you suddenly want to eat leek and pork dumplings. You go to the market and buy the wrappers, leeks and streaky pork. You cut the leeks and ground the meat, ready to make dumplings. Suddenly gay friends call you out to drink milk tea play king, so happy things, how can not go.
However the material is ready, the air is waiting for metamorphic or direct throw away, obviously unreasonable, how can so spoil food! You can put the ingredients in the fridge, and when you’re done, take them out and wrap them again.
- Making dumplings and drinking milk tea to play king, are two things;
- 2, make dumplings the preconditions of this thing is to prepare the wrappers, leeks and meat materials (no material, air bag?)
- 3. In other words, making dumplings depends on an external Context, also known as the Context;
- 4, at this time, gay friends call you drink milk tea king, you need to stop making dumplings this thing;
- Suspend dumpling making and putting the ingredients in the refrigerator is called suspend and save the context.
- 6, after the wave back to continue to pack dumplings and take out the refrigerator material, this is called resume and resume context.
Note:
The suspension and the resumption, that’s your active action, and the blockage, that’s the passive action, like boiling a dumpling, the dumpling is wrapped, but the water hasn’t boiled yet, and you have to wait.
As we know from the example, the context is a pre-requisite resource to complete a transaction. Let me give you another example:
You suddenly have three girlfriends, they also like to eat dumplings, but each has his own taste, Bashi not long like corn, three on youa like cabbage, Hashimoto have food like cilantro.
So, when making a leek dumpling, you will also wrap the other three fillings, and then the business becomes four:
Pack leek dumplings, pack corn dumplings, pack cabbage dumplings, pack coriander dumplings
Gay friends call you the king of milk tea, you need to put these four fillings in the refrigerator (hanging), the problem comes, how to put?
After all, in the future, you will still have seiko Takahashi, Yuka Yamagishi, Miwa Nakamura, Yuka Ono, Sakura Matsushita, Ishihara Hope, and Kuki, right? A simple solution: Put a big bag of resources and put a post-it note with your girlfriend’s name on it
We kept the context unique by marking the filling resource with the girlfriend’s name on the sticky note (immutability). To extract things, dumpling context is as follows:
Post-it note of girlfriend’s name + meat + veggie + something else
The same applies to thread switching contexts:
Thread Id + thread status + stack + register status, etc
Extension to context → CoroutineContext in Kotlin coroutines to store various elements as key-value pairs:
Job + CoroutineDispatcher + ContinuationInterceptor + CoroutineName(CoroutineName, set during debugging)
Wonderful ~
② Structured concurrency
Now that context is the external environment needed to complete a transaction, let’s talk about structured concurrency. We all know:
There is no cascading relationship between threads, threads execute in the context of the entire process, and concurrency is relative to the entire process rather than a parent thread.
This is the unstructuring of threads, whereas from a business perspective:
Each concurrent operation is dealing with a task, which may belong to a parent task or may have its own child task. Each task has its own life cycle, and the life cycle of the child task should inherit the life cycle of the parent task.
This is business structuring, and coroutines in Kotlin are structured concurrency:
In practice, we rarely need a global coroutine, because it is always associated with a local scope in the program. This local scope is an entity with a limited life cycle, such as a network load, and the newly created coroutine object maintains a “cascading relationship” with the parent coroutine.
The specific performance
Coroutines must be started in a scope, which defines rules for parent-child coroutines. Kotlin coroutines control all coroutines in a scope.
Scopes can be parallel or included to form a tree structure. This is the structured concurrency in Kotlin coroutines. Here are the rules:
There are three types of scope subdivision:
- Top-level scope: the scope of a coroutine that has no parent coroutine;
- Cocoroutine: start a new coroutine (child coroutine) in the coroutine, at this time, the scope of the child coroutine is the default cocoroutine scope, uncaught exceptions thrown by the child coroutine will be passed to the parent coroutine processing, the parent coroutine will be cancelled at the same time;
- Master-slave scope: The same as the parent-child relationship of the coscope, except that uncaught exceptions on child coroutines are not passed up to the parent coroutine.
Then there are the rules for parent-child coroutines:
- The parent coroutine is cancelled and all child coroutines are cancelled;
- The parent coroutine will enter the completed state after the execution of the child coroutine, regardless of whether the parent coroutine itself has been executed.
- The child coroutine inherits the elements in the parent coroutine context. If it has a member with the same Key, it overwrites the corresponding Key. The overwriting effect is only valid within its own scope.
③ Collaborative cancellation
Kotlin’s Official Guide to coroutines says: Cancellation is collaborative. What does that mean? Thread cancellation and coroutine cancellation are similar in principle, starting with thread cancellation and then moving to coroutine cancellation.
If I cancel the thread
The reader’s first reaction is not to call Stop () or suspend() on the Thread instance. Yes, but it is not recommended.
The former is inherently unsafe because it is too abrasive and does nothing to clean it up before the thread terminates, while the latter is inherently deadlock-prone.
A better way:
Put a flag -isAlive() in your Thread class that controls whether the target Thread is active or stopped.
If the flag tells it to stop running, make it end the run() method; If the target thread is waiting too long, use the interrupt() method to interrupt the wait.
Transitioning to the Kotlin coroutine is the same, except that:
Flag bit isAlive() → isActive, interrupt method interrput() → Cancel ()
The Job section will explain how to cancel jobs
0x2. Add dependencies
Kotlin coroutines are not included in the Kotlin standard library stdlib, and need to be imported as needed, for example:
// Core library: necessary! The public API, the interface of coroutines in each platform is unified implementation "org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.3.8"/library/platform: The current platform corresponding platform libraries, coroutines is there are differences in the performance of the concrete platform way (like Android) implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines - Android: 1.3.8' / / test library: Coroutines testing library, convenient developers using coroutines in test implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines - test: 1.3.8'Copy the code
Note: The version number of the library must be the same, for example, 1.3.8. The latest version number and more information about the library can be found in the official Github repository: github.com/Kotlin/kotl…
IDEA uses Maven to import Kotlin coroutine dependencies:
Click File → Project Structure → Modules → + → Library… – > the From Maven…
Enter the keywords into the library, click OK, and wait for the download to complete.
0x3 interpretation of the first official Demo
Kotlin coroutine Demo: Your First Coroutine
The output is as follows:
Explain a wave (some nouns do not understand also does not matter, does not affect the follow-up study) :
As mentioned in the previous section, the coroutine of the Kotlin-JVM is a pseudo-coroutine, which is just a nice wrapper around the underlying Thread. Thread.currentthread ().name Defaultdispatcher-worker-1, blind guess thread pool, after all, efficient multithreaded scheduling is basically dependent on thread pool.
Globalscope.launch is simply interpreted as creating a coroutine, and line 12 executes before line 8: creating a thread pool takes time, so it’s not as fast as the main thread’s synchronized code.
Delay () is a suspend function that delays the coroutine without blocking the thread; Thread.sleep() blocks the current Thread;
Suspend means that the coroutine scope is suspended, but code outside the coroutine scope is not blocked in the current thread. Suspend suspends a function. It can only be called from a coroutine or from another suspend function.
When the coroutine is suspended (waiting), the thread is returned to the thread pool, and when the wait is over, the coroutine is recovered from an idle thread in the thread pool.
Can Thread. Sleep (2000L) be removed from line 13?
A: No, this sentence blocks the main thread and keeps the JVM alive until the coroutine completes. If removed, the JVM exits before the coroutine completes.
One more thing:
The main thread is just a normal user thread. All other threads are started by the main thread, but at the process level: Threads are flat, there is no parent-child relationship, and the JVM exits after all user threads have finished executing. The JVM does not care whether a daemon thread is alive or dead, it can be set to a daemon thread by setDaemon(true).
Open the Kotlin codec source code and search globally: isDaemon = true.
1) runBlocking
In addition, Kotlin provides a mechanism to block threads, achieving the same effect as Thread.sleep, with modified code:
The mechanism behind it:
The runBlocking function sets up a coroutine that blocks the current thread, and the main thread waits for the code in runBlocking to complete.
The code above can be improved one more time:
RunBlocking is a global function that can be called anywhere, but is rarely used in projects because blocking the main thread makes little sense and is often used for unit testing to prevent JVM exits.
Another point is that delay() can be used to delay the wait, but it is not a good solution. In real development, there is uncertainty about the time of time-consuming tasks, which can be achieved by using jobs provided by Kotlin coroutines, which will be covered in a moment.
0x4, CoroutineScope → CoroutineScope
① GlobalScope → Global coroutine scope
Click on GlobalScope’s source code:
As a singleton object, there is only one instance of the object in the entire JVM virtual, the life cycle of the entire JVM, so you need to watch out for memory leaks!! As mentioned above, The Kotlin coroutine implements the need for “structured concurrency” through scope, and the coroutine scope can be customized to suit our needs.
② Customize the scope
GlobalScope inherits the CoroutineScope interface, which is relatively simple and holds a CoroutineContext context:
A class that implements this interface can be called a coroutine scope, as shown in the following example:
Use the MainScope() function
For easy use in Android/JavaFx scenarios, the MainScope() function is provided to quickly create mainline-based coroutine scopes.
MainScope can easily control the cancellation of all coroutines within its scope. It is recommended that we define an abstract Activity, as shown in the following example:
It is designed to create sub-scopes using the coroutineScope() and container ()
If an exception occurs in an existing coroutine scope, it will throw an exception (the parent coroutine and other child coroutines will be cancelled). If an exception occurs in an existing coroutine scope, it will not affect other child coroutines.
0x5, Create coroutine → scope function
Coroutine scope determines the parent-child relationship between coroutines and propagation behavior in terms of cancellation or exception handling. Next, you can use scoped functions to create coroutines.
(1) launch & async
These two functions create a new coroutine that “does not block” the current thread. The difference is:
- Launch returns a “Job” for coroutine supervision and cancellation, for scenarios with no return value.
- Async returns a subclass of Job “Deferred”, which can be obtained with await().
An example of simple code use is as follows:
The following output is displayed:
Suspend keyword → suspend function
The Kotlin coroutine provides the suspend keyword, which is used to define a suspend function, which is a tag
When you write a normal function that needs to “suspend and resume at certain times”, add it and leave the rest alone!!
And what it really does:
Tell the compiler that the function needs to be executed in a coroutine, and the compiler converts the suspended function into an optimized version of the callback using a finite state machine.
Extract a wave of business code and define the suspend function using suspend. The modified code looks like this:
0x7, Job → Job
Calling the launch function returns a Job object that represents the work task of a coroutine
(1) the commonly used API
/** * Coroutine status */ isActive: Boolean // cancelled: Boolean // isCompleted: Boolean // completed: children: CancelAndJoin () cancelAndJoin() cancelAndJoin() cancelAndJoin() CancelChildren () // CancellationException can be passed as cancellation reason attachChild(child: ChildJob) // Attach a child coroutine to the current coroutineCopy the code
② Life cycle
The life cycle of a Job consists of a number of states:
New (newly created), Active (Active), Completing (Completing),
(1) Completed, Cancelling, Cancelled
Note the await children in the figure above, when the coroutine is in completed or cancelled, will wait for all the child coroutines to complete before entering the completed or cancelled state.
③ Cancel operation details
- Canceling a scope cancels all of its subcoroutines;
- Cancelled subcoroutines in the same scope do not affect other sibling coroutines.
- The coroutine handles cancellations by throwing a special CancellationException, which is created by default in the cancel function, or by building new instances of itself. The subcoroutine is canceled because of CancellationException. The parent coroutine does not require any additional operations;
- Cannot start a new coroutine in a cancelled scope;
- The cancellation of coroutines is “collaborative”. The coroutine does not stop immediately when cancel() is called. After the call, the coroutine only enters the cancelled state and only becomes cancelled after the work is completed. For example:
The output is as follows:
It does not stop immediately after cancellation, so we need to determine manually, for example by adding **isActive** to the while() loop
while(i < 5 && isActive)
Copy the code
You can also check this by using **ensureActive(), which will raise an exception if the Job is inactive, in the first line of the loop body. You can also use yield()** to check that the first operation is to check if the Job is complete, which will throw a CancellationException to exit the coroutine.
Cancelling a coroutine throws a CancellationException, which can be caught using a try/catch block and cleaned up ina finally block such as resource release.
But there’s a catch: A coroutine in the canceled state cannot be suspended. If the code in finally involves suspension, subsequent code will not continue to execute. You can create an uncancellable task with withContext + NonCancellable to complete the cleanup task.
The running results are as follows:
④ Exception Handling
There are three ways to handle exceptions in Kotlin: first, “try-catch directly catches exceptions in scope”. The code example is as follows:
The following output is displayed:
Note: you cannot use try-catch to catch launch and async scoped exceptions!!
Then is in the “global exception handling” with Rx RxJavaPlugins. SetErrorHandler capture exception is similar to the global coroutines scope is nested children parent relationship, so abnormal could, in turn, throw multiple, code samples are as follows:
The following output is displayed:
Note: only launch() incoming is supported, async() incoming is invalid; Global exception handling does not prevent coroutine cancellation, only preventing an exception from exiting the program.
Finally, “exception propagation”. By default, exception propagation in coroutine scope is bidirectional as follows:
- If the parent coroutine is abnormal, all child coroutines are cancelled.
- If the child coroutine is abnormal, the parent coroutine will be cancelled, and the sibling coroutine will be cancelled indirectly.
A code example is as follows:
The following output is displayed:
There are two ways to make propagation unidirectional, that is, an exception in a child coroutine does not affect the parent coroutine and its sibling coroutine. One way it is designed is to replace the Job with a SupervisorJob. Example of the code modified:
The other is the container it is handling with the custom scope introduced above, which is shown in the following code example:
Same run result:
⑤ Start mode
The source code of launch & Async is cut off from launch & Async, and the second parameter CoroutineStart is clicked into the source code:
Public enum class CoroutineStart {// By default, the system starts to schedule data immediately after the system is created, and the system is canceled before the system is scheduled. DEFAULT, // lazy load, will not start scheduling immediately, you need to manually call start, join or await to start scheduling, if canceled before scheduling, coroutine will directly enter the abnormal end state. LAZY, // similar to Default, starts scheduling immediately and does not respond to cancellation until a suspended function is executed. / / involves cancle makes sense @ ExperimentalCoroutinesApi ATOMIC, / / directly in the current thread execution coroutines body, until you hit the first hang function, Will dispatch to the thread / / specified scheduler performed on @ ExperimentalCoroutinesApi UNDISPATCHED; }Copy the code
0x8 Scheduler → CoroutineDispatcher
① Four types of schedulers
The Kotlin coroutine presets four types of schedulers, as shown in the following table:
species | describe |
---|---|
Default | By default, thread pools are suitable for processingThe background to calculate, CPU intensive task scheduler |
IO | IO schedulers, suitable for IO related operations, IO intensive task scheduler |
Main | The scheduler UI, the scheduler that is initialized to the corresponding UI thread depending on the platform, such as Android’s main thread |
Unconfined | No thread is specified. If a subcoroutine switches threads, subsequent code continues on that thread |
In addition, the scheduler provides an attribute **immediate**, which executes the scheduler switch without performing it if you are currently in the scheduler. The following is an example of comparison:
CoroutineScope (Job () + Dispatchers. Main. Immediate). The launch first performed} {/ / / / the second execution CoroutineScope (Job () + Dispatchers.main).launch {// Scheduler switch, cause slower so fourth execution} // third executionCopy the code
(2) withContext
Unlike launch, async, and runBlocking, withContext does not create a new coroutine and is often used to switch the thread on which code execution is running. It is also a method that suspends until the result is returned. Multiple WithContexts are executed sequentially, so it is good for cases where one task depends on the result returned by the previous task, such as:
Async +await = async+await = async+await = async+await
0x9, Interceptor → ContinuationInterceptor
As the name implies, it is used to intercept coroutines to do some additional operations, such as the scheduler above, which is implemented with interceptors. Try writing a Demo that intercepts logs, where continuations are objects that hold the pending state of coroutines and local variables.
The running results are as follows:
Four intercepts occurred, in order: when the coroutine was started (the first two), when it was suspended, and when the result was returned. We could write a simple version of the thread scheduler that allows the coroutine to switch threads at startup, as shown in the following example:
The running results are as follows:
As you can see, the coroutine switches to running in a custom thread pool, with withContext, and then it cuts back.
0 x10, Deferred
Take a look at the Deferred source code:
On top of inheriting the Job,
is specified to output the generic, await() suspends the coroutine and returns the final execution result.
0x11, Channel → Channel
Similar to BlockingQueue in Java for multithreaded data transfer, Kotlin coroutines provide channels for data transfer between multiple coroutines. Elements are added from one end and consumed from the other. In addition to blocking operations, channels also provide non-blocking send and receive operations. A simple usage code example is as follows:
In addition, Receiver supports for iteration to receive messages, such as: for(C in channel) is also possible.
Note: Call close() to close a Channel when you’re done with it, otherwise the coroutine reading from the Channel will hang indefinitely, waiting for data to come in!!
① Different Channel types
Open Channel source:
Inheriting SendChannel and ReceiveChannel, we define several constants representing the Channel type:
RENDEZVOUS → default, 0 cache, created a RENDEZVOUS channel, send is suspended until received; UNLIMITED → Create a LinkedListChannel with UNLIMITED capacity and send will not hang; BUFFERED → set size to ArrayChannel; CONFLATED → Create a ConflatedChannel. The new send overwrites the previous one, and the receiver only gets the latest one. // Create different types of channels: val rendezvousChannel = Channel<String>() val unlimitedChannel = Channel<String>(UNLIMITED) val bufferedChannel = Channel<String>(10) val conflatedChannel = Channel<String>(CONFLATED)Copy the code
(2) SendChannel & ReceiveChannel
SendChannel defines the interface for sending data to a channel. ReceiveChannel defines the interface for receiving data from a channel.
/* === SendChannel === */ isClosedForSend: Boolean Throwable? = null): Boolean // Close channel send(element: E) // suspend the function, if the channel is full, this function will suspend the execution of the offer(element: E): Boolean // Send data synchronically, channel full or closed cannot be added successfully, return false or throw exception invokeOnClose(handler: (cause: Throwable?) -> Unit) // Channel closure SelectClause2<E, SendChannel<E>> // Send data immediately (if allowed), using /* === ReceiveChannel === */ isClosedForReceive in the SELECT expression: False isEmpty: Boolean Cancel (cause: CancellationException? ReceiveOrClosed () = null) // Close the channel receive(): E // suspend the function, receive data, if the channel is closed will throw an exception receiveOrClosed(): ValueOrClosed<E> // same as above, except that the channel is closed and does not throw an exception, but returns null poll(): SelectClause1<E> // Used for select expressionsCopy the code
Channel, Flow and practical application
References:
- “Translate” reveals coroutine context
- Understand Kotlin’s coroutine
- Coroutines in cancel and abnormal | cancel, rounding operation
- Coroutines of cancellation, and an abnormal | processing steps
- End of Kotlin coroutine life – coroutine cancellation
- What happens when a problem occurs in the schedule? – Coroutine exception
- Understanding Kotlin Coroutines in Depth. By Binggan Huo