[kotlin coroutine] Wanzi coroutine, a complete kotlin coroutine advanced
Kotlin Coroutine Advanced
- Introduction to coroutines
- 1. Basic use of coroutines
-
- 1.1, runBlocking startup
- 1.2, GlobalScope .launch startup
- 1.3, GlobalScope.async startup
- 1.4, description of three startup methods
- Second, Coroutine source code analysis
-
- 2.1, CoroutineContext
- 2.2, Job source code
- 2.3, Job’s Commonly used functions
- 2.4, SupervisorJob
- Third, suspend keyword
-
- 3.1, CoroutineDispatcher scheduler
- 3.2, CoroutineStart coroutine startup mode
- 3.3, CoroutineScope – Coroutine Scope
- 3.4, Classification and Behavior Rules
- Fourth, the use and cancellation of coroutines and exceptions in Android
-
- 4.1, using SupervisorJob
- 4.2, using supervisorScope
Introduction to coroutines
Kotlin provide a new way to handle concurrency, which can be used on the Android platform to simplify asynchronously executed code. Coroutines have been introduced since Kotlin 1.3, but this concept has existed since the dawn of the programming world. The earliest programming language using coroutines can be traced back to 1967 Years of the Simula language.
In the past few years, the concept of coroutines has developed rapidly and has been adopted by many mainstream programming languages, such as Javascript, C#, Python, Ruby, Go and more. Kotlin‘s coroutines are based on established concepts from other languages.
On the Android platform, coroutines are mainly used to solve two problems:
- Processing time-consuming tasks (Long running tasks), such tasks often block the main thread;
- Ensure that the main thread is safe (Main- safety) , which ensures that any suspend functions are safely called from the main thread.
In essence, a coroutine is a lightweight thread.
1. Basic use of coroutines
Before using the coroutine, we need to introduce the Coroutine package
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
// coroutine core library
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// coroutine Android support library
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
// coroutine Java8 support library
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3"
There are many ways to create coroutines. Here we will not extend the advanced usage of coroutines (hot data channel Channel, cold data flow Flow.…), create a coroutine Here are three commonly used methods:
1.1, runBlocking start
runBlocking {
println("runBlocking starts a coroutine")
}
runBlocking Starting a coroutine will block the calling thread until the code inside is executed, and the return value is genericT.
1.2, GlobalScope.launch start
GlobalScope.launch {
println("launch starts a coroutine")
}
launch Starting a coroutine will not block the calling thread, it must be called in the coroutine scope (CoroutineScope), and the return value is a Job
1.3, GlobalScope.async start
GlobalScope. async {
the pricentln("async starts a coroutine")
}
async Starting a coroutine is actually the same as launch, the difference is that the return parameter of async is: Deferred Deferred : Job
, which implements a Deferred interface, but Deferred inherits job. The difference between Deferred and job is that Deferred defines The await function needs to be used in conjunction with the await()
suspending function.
1.4, description of three startup methods
-
runBlocking{} – primarily for testing
This method is designed to allow libraries written in the suspend style Can be used in regular blocking code, often in main methods and tests.
-
GlobalScope.launch/async{} – not recommended
Because the coroutine launched in this way exists The situation where the component has been destroyed but the coroutine still exists may lead to resource exhaustion in extreme cases, so it is not recommended to start this way, especially in scenarios where components need to be frequently created and destroyed on the client side.
2. Coroutine source code analysis
Here we use the source code of CoroutineScope.launch{}
as an example to learn more about Coroutine:
The above is launchfunction, which appears as an extension function of
CoroutineScope
, the function parameters are: coroutineContext
, coroutineStart
code>, coroutine body
, the return value is coroutine instance Job
, where CoroutineContext
includes Job
, CoroutineDispatcher
, CoroutineName
. Below we will introduce these contents one by one: CoroutineContext
, Job
, CoroutineDispatcher
, CoroutineStart
, CoroutineScope
.
2.1, CoroutineContext
CoroutineContext: Coroutine context
- Thread behavior, life cycle, exception and debugging
- Contains some user-defined data collections, which are closely related to coroutines
- It is an indexed Element instance collection, a data structure between set and map. Each element has a unique Key in this collection
-
Job: Control the life cycle of coroutines
-
CoroutineDispatcher: Distribute tasks to appropriate threads
-
CoroutineName: The name of the coroutine, which is useful for debugging
-
CoroutineExceptionHandler: Handling uncaught exceptions
CoroutineContext
has two very important elements — Job
and Dispatcher
, Job
is the current Coroutine
instance and Dispatcher
determines the current Coroutine
execution thread, You can also add CoroutineName
for debugging, add CoroutineExceptionHandler
for catching exceptions, and they all implement the Element
interface.
fun main() {
val coroutineContext = Job() + Dispatchers. Default + CoroutineName("myContext")
println("$coroutineContext,${coroutineContext[CoroutineName]}")
val newCoroutineContext = coroutineContext. minusKey(CoroutineName)
println("$newCoroutineContext")
}
Output:
CoroutineContext source code:
Through the source code, I can see that CoroutineContext
defines four core operations:
- Operator get
can get thisElement
throughkey
. Since this is aget
operator, it can be accessed in the form of square bracketscontext[key]
like accessing elements in a map. - The operator plus
is similar to theSet.plus
extension function, returning a newcontext
object, The new object contains all theElement
in the two. If there is a duplicate (the same Key), then use theElement
on the right side of the+
number code> replaces the one on the left. The+
operator can easily be used to combine contexts, but there is one very important thing to be careful about - pay attention to the order in which they combine, because this+
operator is asymmetrical. - fun fold(initial: R, operation: (R, Element) -> R): R
andCollection.fold
extension Functions are similar, providing the ability to traverse allElement
in the currentcontext
. - fun minusKey(key: Key): CoroutineContext
Returns a context that contains elements in that context, but does not contain elements with the specifiedkey
element.
2.2, Job source code
Job is used to process coroutines
For each coroutine created (via launch
or async), it will return a Job
instance, which is the unique identifier of the coroutine and is responsible for Manage the lifecycle of coroutines
The CoroutineScope.launch
function returns a Job
object, representing an asynchronous task. Job
has a lifecycle and can be canceled. Job
can also have a hierarchical relationship, a Job
can contain multiple child Job
, when the parent Job
is canceled, All child Job
will also be automatically canceled; when the child Job
is canceled or an exception occurs, the parent Job
will also be canceled.
In addition to creating a Job
object through CoroutineScope.launch
, you can also create the object through the Job()
factory method. By default, failure of a child Job
will cause the parent Job
to be canceled, this default behavior can be modified by SupervisorJob
.
A parent Job
with multiple child Job
will wait for all child Job
to complete (or cancel) before executing itself
Status of the Job Lifecycle
A task can have a range of states: newly created (New), active (Active), completing (Completing), Completed, Canceling (Cancelling), and Canceled (Cancelled). While we can't access these states directly, we can access the properties of Job
: isActive
, isCancelled
, and isCompleted
.
If the coroutine is in the active state, the coroutine running error or calling job.cancel()
will put the current task into the canceling (Cancelling) state (isActive = false, isCancelled = true
). When all child coroutines are completed, the coroutine will enter the canceled (Cancelled) state, at this time isCompleted = true
.
2.3, common functions of Job
These functions are thread-safe, so they can be called directly from other
Coroutine
.
-
fun start(): Boolean
Call this function to start thisCoroutine
, if the currentCoroutine
Calling this function returnstrue
if it has not been executed yet. If the currentCoroutine
has been executed or has been executed, calling this function returnsfalse
-
fun cancel(cause: CancellationException? = null)
Cancels this job with an optional cancellation reason. The reason can be used to specify an error message or provide additional details about the reason for the cancellation, for debugging. -
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
Through this function you can set a completion forJob
Notification, this notification function will be executed synchronously when the execution ofJob
is completed. The notification object type of the callback is:typealias CompletionHandler = (cause: Throwable?) -> Unit
. TheCompletionHandler
parameter represents how theJob
is executed of.cause
has the following three situations: -
- If the
Job
is executed normally, thecause
parameter isnull
- If the
-
- If the
Job
was canceled gracefully, thecause
parameter is aCancellationException
object. This situation should not be treated as an error, it is a case of normal cancellation of the task. So there is generally no need to record this situation in the error log. - Other cases indicate that
Job
failed to execute.
- If the
The return value of this function is a DisposableHandle
object. If you no longer need to monitor the completion of Job
, you can call DisposableHandle.dispose
function to cancel listening. If the Job
has been executed, there is no need to call the dispose
function, and the monitoring will be canceled automatically.
- suspend fun join()
join
function is different from the previous three functions, this is a suspend
function. So it can only be called within a Coroutine.
This function will suspend the current Coroutine
until the execution of the Coroutine
is completed. So the join
function is generally used in another Coroutine
to wait for the completion of job
to continue execution. When the execution of Job
is completed, the job.join
function resumes. At this time, the task of job
is already in the completed state, and calling job The
continues to be in the Coroutine
of the .joinactivie
state.
Note that a job is not complete until all of its children are complete
The suspension of this function can be canceled, and always check whether the Job
of the Coroutine
called is cancelled. If the Job
that called the Coroutine
is canceled or completed when this suspend function is called or suspended, this function will throw a CancellationException
.
2.4, Supervisor Job
SupervisorJob
is a top-level function defined as follows:
This function creates a supervisor job
in the active
state. as beforeAs mentioned above, Job
has a parent-child relationship. If the child Job
fails, the parent Job
will fail automatically. This default behavior may not be ours Expected. For example, there are two child Job
in Activity
to obtain the comment content and author information of an article respectively. If one of them fails, we don't want the parent Job
to be canceled automatically, which will cause the other child Job to be canceled as well. And SupervisorJob
is such a special Job
, the child Job
does not affect each other, a child Job
fails , does not affect the execution of other child Job
. SupervisorJob(parent:Job?)
has a parent
parameter, if this parameter is specified, the returned Job
is the parameter parent
child Job
. If the Parent Job
fails or is cancelled, the Supervisor Job
will also be cancelled. When a Supervisor Job
is canceled, all child Job
of Supervisor Job
will also be canceled.
MainScope()
is implemented using SupervisorJob
and a Main Dispatcher
:
But SupervisorJob
is easy to be misunderstood. It is related to coroutine exception handling and sub-coroutines.The >Job type and domain are confusing. For specific exception handling, please refer to this Google article: Cancellation and Exceptions in Coroutines | Exception Handling Details
Three, suspend keyword
This suspend
keyword, since it does not really implement suspension, what is its function?
It is actually a reminder.
The creator of the function reminds the user of the function: I am a time-consuming function, and I am put in the background by my creator in a suspended manner, so please call me in the coroutine.
The suspended operation—that is, thread cutting, depends on the actual code in the suspended function, not on this keyword.
So this keyword, is just a reminder.
3.1, CoroutineDispatcher scheduler
-
Dispatchers.Default
The default scheduler is suitable for processing background calculations and is a CPU-intensive task scheduler. If no dispatcher is specified when creating a Coroutine, this is generally used as the default value by default. The Default dispatcher uses a shared pool of background threads to run its tasks. Note that it shares the thread pool with IO, but only limits the maximum number of concurrency.
-
Dispatchers.IO
As the name implies, this is used to perform blocking IO operations, and shares a shared thread with Default Pool to perform tasks inside. Depending on the number of tasks running at the same time, additional threads will be created when needed, and unneeded threads will be released when the tasks are completed.
-
Dispatchers.Unconfined
Since Dispatchers.Unconfined does not define a thread pool, it starts the thread by default during execution. The first suspension point is encountered, after which theThe thread calling resume determines the thread that resumes the coroutine.
-
Dispatchers.Main
The specified execution thread is the main thread, which is the UI thread on Android.
Because the child Coroutine will inherit the context of the parent Coroutine, for the convenience of use, we generally set a Dispatcher on the parent Coroutine, and then all child Coroutines will automatically use this Dispatcher.
3.2, CoroutineStart coroutine startup mode
-
CoroutineStart.DEFAULT
Start scheduling immediately after the coroutine is created, if the coroutine is canceled before scheduling, it will directly enter the cancellation response Although the state is scheduled immediately, it may also be canceled before execution
-
CoroutineStart.ATOMIC
Immediately after the coroutine is created Start scheduling, and the coroutine execution will not respond to cancellation before reaching the first suspension point
Although it is immediate scheduling, it combines the two steps of scheduling and execution into one, just like its name, it guarantees scheduling And execution is an atomic operation, so the coroutine will also be executed -
CoroutineStart.LAZY
As long as the coroutine is needed , including actively calling the start, join or await functions of the coroutine, the scheduling will start. If it is canceled before scheduling, the coroutine will directly enter the abnormal end state
-
CoroutineStart.UNDISPATCHED
Immediately after the coroutine is created, it is executed in the current function call stack until it encounters the first real suspension point
is executed immediately, so The coroutine will definitely execute
The design of these startup modes is mainly to deal with some special scenarios. business developmentIn practice, it is usually enough to use the two startup modes DEFAULT and LAZY
3.3, CoroutineScope – coroutine scope
Defining a coroutine must specify its CoroutineScope
. CoroutineScope
can track the coroutine even if the coroutine is suspended. Unlike the dispatcher (Dispatcher
), CoroutineScope
doesn’t run coroutines, it just makes sure you don’t lose track of them. To ensure that all coroutines are tracked, Kotlin
does not allow new coroutines to be started without using CoroutineScope
. CoroutineScope
can be thought of as a lightweight version of ExecutorService
with superpowers. CoroutineScope
keeps track of all coroutines, and it can also cancel all coroutines started by it. This is very useful in Android
development, for example, it can stop the execution of the coroutine when the user leaves the interface.
Coroutine
is a lightweight thread, which does not mean that it does not consume system resources. When the asynchronous operation is time-consuming, or when an error occurs in the asynchronous operation, the Coroutine
needs to be canceled to release system resources. In the Android
environment, usually the Coroutine
started by each interface (Activity
, Fragment
, etc.) Makes sense, if the user exits the interface while waiting for the Coroutine
to execute, it may not be necessary to continue executing the Coroutine
. Additionally Coroutine
also needs to be executed in a proper context
, otherwise errors will occur, such as accessing View
in a non-UI
thread. Therefore, when designing Coroutine
, it is required to be executed within a scope (Scope
), so that when this Scope
is cancelled, all the child Coroutine
is also automatically canceled. So to use Coroutine
you must first create a corresponding CoroutineScope
.
CoroutineScope interface
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineScope
just defines a new Coroutine
execution Scope
. Each coroutine builder
is an extension function of CoroutineScope
and automatically inherits the coroutineContext
of the current Scope
.
3.4, classification and rules of conduct
The official framework also provides a scope in the process of implementing composite coroutines, which is mainly used to clarify the parent-child relationship between writing, as well as the propagation behavior for cancellation or exception handling. This scope includes the following three types:
-
Top-level scope
The scope of a coroutine without a parent coroutine is the top-level scope. -
Synergy Scope
A new coroutine is started in a coroutine, and the new coroutine is a sub-coroutine of the existing coroutine. In this case, the scope of the sub-coroutine is the collaboration scope by default. At this time, the uncaught exception thrown by the child coroutine will be passed to the parent coroutine for processing, and the parent coroutine will also be canceled at the same time.coroutineScope The internal exceptions will be propagated upwards, and the uncaught exceptions of the sub-coroutines will be passed up to the parent coroutines. Any abnormal exit of a sub-coroutine will lead to the overall exit
-
Master-slave role Domain
is consistent with the synergy scope in the parent-child relationship of the coroutine, the difference is that when the coroutine under this scope has an uncaught exception, the exception will not be passed up to the parent coroutine .supervisorScope belongs to the master-slave scope and will inherit the context of the parent coroutine. Its characteristic is that the exception of the child coroutine will not affect the parent coroutine
In addition to the behaviors mentioned in the three scopes, there are the following rules between parent and child coroutines:
- If the parent coroutine is canceled, all child coroutines will be cancelled. Since there is a parent-child coroutine relationship in both the coroutine scope and the master-detail scope, this rule applies to both.
- The parent coroutine needs to wait for the child coroutine to finish executing before finally entering the completion state, regardless of whether the coroutine body of the parent coroutine itself has been executed.
- The child coroutine will inherit the elements in the coroutine context of the parent coroutine. If it has members with the same
key
, it will overwrite the correspondingkey
, the overriding effect is only valid within its own range.
Fourth, the use and cancellation of coroutines and exceptions in Android
If an ordinary coroutine generates an unhandled exception, it will propagate the exception to its parent coroutine, and then the parent coroutine will cancel all child coroutines, cancel itself, and continue to pass the exception upwards. Let’s take an official picture to illustrate this process:
Sometimes this situation is not what we want. We hope that when a coroutine generates an exception, it will not affect the execution of other coroutines. We have also mentioned some solutions above, and we will introduce them below Practice it.
4.1, using SupervisorJob
// Use MainScope() of the official library to obtain a coroutine scope for creating coroutines
private val mScope = MainScope();
fun onClickCoroutine(view: View) {
mScope.launch(Dispatchers.Default) {
println("I am the first coroutine")
}
mScope.launch(Dispatchers.Default) {
println("I am the second coroutine")
throw RuntimeException("RuntimeException is an exception")
}
mScope.launch(Dispatchers.Default) {
println("I am the third coroutine")
}
}
Code execution result:
MainScope()
mentioned before, its implementation is to use SupervisorJob
. The execution result is that the second coroutine throws an exception, the third coroutine executes normally, But the program crashed, because we didn’t handle this exception, let’s improve the code
Exception handling:
fun onClickCoroutine(view: View) {
mScope.launch(Dispatchers.Default) {
println("I am the first coroutine")
}
mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
println(
"CoroutineExceptionHandler: $throwable"
)
}) {
println("I am the second coroutine")
throw RuntimeException("RuntimeException is an exception")
}
mScope.launch(Dispatchers.Default) {
println("I am the third coroutine")
}
}
Print result:
The program does not crash, and the exception handling print is also output, which achieves the effect we want. But pay attention to one thing, the parent of these sub-coroutines is SupervisorJob
, but if they have sub-coroutines, the parent of their sub-coroutines is not SupervisorJob, so when they generate When it is abnormal, it is not the effect we demonstrated. We use an official diagram to explain this relationship:
As shown in the figure, when a new coroutine is created, a new Job
instance will be generated to replace SupervisorJob
.
4.2, using supervisorScope
This scope is also mentioned above, using supervisorScope
can also achieve the effect we want, the above code:
fun onClickCoroutine(view: View) {
val coroutineScope = CoroutineScope(Job() + Dispatchers. Default)
coroutineScope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
println(
"CoroutineExceptionHandler: $throwable"
)
}) {
supervisorScope {
launch {println("I am the first coroutine")
}
launch {
println("I am the second coroutine")
throw RuntimeException("RuntimeException is an exception")
}
launch {
println("I am the third coroutine")
}
}
}
}
Run results
It can be seen that the effect we want has been achieved, but if we replace supervisorScope
with coroutineScope
, the result is not like this. In the end, I still use the official picture to show it:
This is the end of the article. This article refers to Quyunshuo. If there is any infringement, please contact to delete it.