There are a lot of articles about Kotlin coroutines, most of which are translated in accordance with the official tutorial, and many concepts are quite confusing to understand, especially the exception handling part of coroutines. So I plan to follow the official documents and excellent Kotlin coroutine articles to systematically learn.
A coroutine is a concurrent design pattern that you can use on the Android platform to simplify code that executes asynchronously. Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages.
The characteristics of
Coroutines are our recommended solution for asynchronous programming on Android. Notable features include:
- Lightweight: You can run multiple coroutines on a single thread because coroutines support suspension and do not block the thread running them. Suspension saves memory than blocking and supports multiple parallel operations.
- Fewer memory leaks: Multiple operations are performed within a scope using structured concurrency mechanisms.
- Built-in cancel support: Cancel operations are automatically propagated throughout the running coroutine hierarchy.
- Jetpack Integration: Many Jetpack libraries include extensions that provide full coroutine support. Some libraries also provide their own coroutine scope that you can use to structure concurrency.
The concept of coroutines is very simple, but it is a little difficult to understand. Let’s start with two simple questions
What is a coroutine
Coroutines are not a concept Kotlin invented. See implementations of coroutines at the level of other languages. Coroutines are a programming idea that is not limited to any language.
The core function of coroutine is to simplify the asynchronous code. To put it bluntly, coroutine simplifies the original complex asynchronous thread, making the logic clearer and the code more concise
Here, we must compare threads and coroutines to understand their direct relationship from the perspective of Android developers:
- Our code runs in a thread, and a thread runs in a process
- Coroutines are not threads; they also run in threads, whether single-threaded or multithreaded
- In a single thread, using coroutines does not reduce the execution time of the thread
So how do coroutines simplify asynchronous code? Let’s start with the most classic use of coroutines – thread control
callback
In Android, the most common way to handle asynchronous tasks is to use callback
public interface Callback<T> {
void onSucceed(T result);
void onFailed(int errCode, String errMsg);
}
Copy the code
The characteristics of callback are obvious
- Advantages: Easy to use
- Disadvantages: If the business is much, it is easy to fall into callback hell, nested logic complex, high maintenance
RxJava
So what’s the solution? It’s natural to think of RxJava
-
Advantages: RxJava uses chained calls to switch threads and eliminate callbacks
-
Disadvantages: RxJava is difficult to get started, and various operators, easy to abuse, high complexity
As an extended library of Kotlin itself, coroutines are simpler and more convenient to use
Now use coroutines to make network requests
launch {
val result = get("https://developer.android.com")
print(result)
}
}
suspend fun get(url: String) = withContext(Dispatchers.IO) {
//network request
}
Copy the code
The code snippet is shown here. Launch is not a top-level function, so let’s just focus on the specific logic inside {}
The above two lines of code execute in two separate threads, but look the same as a single thread.
You get (” https://developer.android.com “) is a hang function, can guarantee after the request, to print the results, this is the most core of coroutines non-blocking type hung
How do coroutines work
So in the coroutine suspend, what exactly is suspended? So let’s see how coroutines work first, and then how do we use them
Coroutine basics
As mentioned above, launch is not a top-level function, so how do you actually create coroutines?
// Use the runBlocking top-level function
runBlocking {
get(url)
}
// Create a CoroutineScope object via CoroutineContext and launch the coroutine
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
get(url)
}
GlobalScope is a subclass of CoroutineScope, which is essentially CoroutineScope
GlobalScope.launch {
get(url)
}
// Use async to start coroutines
GlobalScope.async {
get(url)
}
Copy the code
- Method one is typically used in unit testing scenarios, and is not used in business development because it is thread blocked.
- Method two is standard usage, which we can pass
context
Parameters to manage and control the coroutine lifecycle (herecontext
It’s not the same thing as Android, it’s a more general concept, and there will be an Android platform wrapper to work with),CoroutineScope
To create the scope of the coroutine - Methods three
GlobalScope
isCoroutineScope
Subclass, use scenarios are not elegant. - The difference between method four and method three is
launch
andasync
This will be analyzed later
CoroutineScope
CoroutineScope is the scope of coroutines in which all coroutines need to be started
CoroutineContext
The persistence context of the coroutine, which defines the following behavior of the coroutine:
-
Job: Controls the life cycle of coroutines.
-
CoroutineDispatcher: Dispatches work to the appropriate thread.
-
CoroutineName: Name of the coroutine, which can be used for debugging.
-
CoroutineExceptionHandler: uncaught exception handling.
Here is a standard coroutine
val ctxHandler = CoroutineExceptionHandler {context , exception ->
}
val context = Job() + Dispatchers.IO + EmptyCoroutineContext + ctxHandler
CoroutineScope(context).launch {
get(url)
}
suspend fun get(url: String){}Copy the code
The launch function, which essentially means: I’m going to create a new coroutine and run it on the specified thread. Who is this “coroutine” that is being created and run? That’s the code you sent launch, and this continuous code is called a coroutine
Another way to think about it is that the concept of coroutines consists of three aspects: CoroutineScope + CoroutineContext+ coroutine
A coroutine is an abstract concept, while a coroutine is a code block of launch or async function closures, a concrete implementation of concurrency, which is the coroutine in question
Using coroutines
The most common feature of coroutines is concurrency, and the typical scenario of concurrency is multithreading. You can use the dispatchers. IO parameter to cut tasks to the IO thread:
coroutineScope.launch(Dispatchers.IO) {
...
}
Copy the code
Switch to the Main thread using dispatchers. Main
coroutineScope.launch(Dispatchers.Main) {
...
}
Copy the code
When do you use coroutines? When you need to cut threads or specify threads. You’re running a mission in the background? Slice!
coroutineScope.launch(Dispatchers.IO) {
val result = get(url)
}
Copy the code
And then need to update the interface in the foreground? Cut again!
coroutineScope.launch(Dispatchers.IO) {
val result = get(url)
launch(Dispatchers.Main) {
showToast(result)
}
}
Copy the code
At first glance, there is nesting
If you just use the launch function, coroutines don’t do much more than threads. But there is a very useful function in coroutines called withContext. This function switches to the specified thread and automatically cuts the thread back to resume execution after the logical execution within the closure has finished.
coroutineScope.launch(Dispatchers.Main) { Val result = withContext(dispatchers.io) {// Switch to the IO thread, Get (url) // Execute on the I/O thread} showToast(result) // Restore to the main thread}
Copy the code
This may not seem like much of a difference, but if you need to do thread switching frequently, it can be an advantage. Consider the following comparison:
// coroutinescope.launch (dispatchers.io) {... launch(Dispatchers.Main){ ... launch(Dispatchers.IO) { ... launch(Dispatchers.Main) { ... }}}}// Implement the same logic by writing the second coroutinescope.launch (dispatchers.main) {... withContext(Dispatchers.IO) { ... }... withContext(Dispatchers.IO) { ... }... }
Copy the code
Depending on the withContext cut-back nature, you can extract the withContext into a separate function
coroutineScope.launch(Dispatchers.Main) { Int int int int int int int int int int int int int int int int int int int int int int int int int int int String) = withContext(Dispatchers.IO) { // to do network request url }
Copy the code
This makes the code logic much clearer
WithContext () does not add any additional overhead compared to the equivalent callback based implementation. In addition, in some cases, you can optimize the withContext() call to perform better than using callbacks. For example, if a function is called ten times on a network, you can use the external withContext() to make Kotlin switch threads only once. This way, even if the network library uses withContext() multiple times, it stays on the same scheduler and avoids switching threads.
If you look carefully, you will notice that both of our examples are missing the suspend keyword.
fun get(url: String) = withContext(Dispatchers.IO) { Suspend function'withContext' should be called only from a coroutine or another Suspend funcion}
Copy the code
This means that withContext is a suspend function, which can only be called from other suspend functions or by using a coroutine builder (such as launch) to start a new coroutine
suspend
Suspend is the key at the heart of the Kotlin coroutine. Code suspends when it reaches the suspend function. The suspend is non-blocking and does not block your current thread.
Suspend is used to compile:
suspend fun get(url: String)= withContext(Dispatchers.IO) { ... }Copy the code
Suspend what exactly is suspend? How does it implement non-blocking suspension?
Suspension of coroutines
What does the coroutine hang on? How to suspend the thread?
You’re actually suspending the coroutine itself. What’s more specific?
As mentioned earlier, a coroutine is simply a block of code in the closure of a launch or async function.
When a coroutine executes to the suspend function, it is suspended.
So where does the coroutine hang from? Current thread
What does it do when it’s suspended? Exits the currently running thread, starts execution on the specified thread, and resumes the coroutine after execution.
The coroutine is not stopped, it is separated from the current thread, the pawns are separated from each other, so what does each do after the separation?
-
thread
When code in the thread reaches the suspend function of the coroutine, the rest of the coroutine code is not executed
-
If the thread is background thread:
- If other background tasks exist, run them
- If there are no other tasks, there is nothing to do, waiting to be collected
-
If it is the main thread:
Then continue to perform the work and refresh the page
-
-
coroutines
The thread’s code is pinchedwhen it reaches the suspend function, and the coroutine proceeds from the suspend function, but on the specified thread.
Who appointed it? Is specified by the suspend function, such as the IO thread specified by the dispatchers. IO passed in by withContext inside the function.
Dispatchers, which can restrict coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unrestricted, more on Dispatchers later
The best thing that coroutines do for us after the suspend function is complete is to automatically cut threads back.
Our coroutine originally runs on the main thread. When the code encounters the suspend function, a thread switch occurs, and the Dispatchers switch to the corresponding thread for execution.
When this function completes, the thread cuts back, and the coroutine posts another Runnable for me, allowing the rest of my code to go back to the main thread.
Here borrow Google official picture, very vivid expression of the effect of hanging
The essence of coroutine suspension is to cut a thread
However, the difference between suspending coroutines and using Handler or Rxjava is that when the suspending function completes, the coroutine automatically cuts back to the original thread.
The cut back action, the resume in the coroutine, must be in the coroutine in order to resume
This also explains why the suspend function needs to be called from either a coroutine or another suspend function, all in order to allow the suspend function to switch threads and then cut back again, right
How do coroutines hang
How is the suspend function suspended? Does the suspend directive do that? Here’s a suspend function to try it out:
suspend fun printThreadInfo(a) { print(Thread.currentThread().name)}I/System.out:main
Copy the code
Suspend function is defined. Why is the coroutine not suspended?
Compare the previous example:
suspend fun get(url: String)= withContext(Dispatchers.IO) { ... }Copy the code
The difference is the withContext function. If you look at the withContext source code, you can see that it is itself the suspend function. It receives a Dispatcher parameter. Depending on the Dispatcher parameter, your coroutine is suspended and then cut to another thread.
So you cannot suspend coroutines. It is the coroutine framework that does suspend coroutines. To suspend coroutines, you must directly or indirectly use the coroutine framework’s suspend function
The role of suspend
The suspend keyword does not actually suspend, so what does it do?
It’s actually a reminder.
Note to users of this function: I am a time-consuming function that was suspended in the background by my creator, so please call me in the coroutine.
Why does the suspend keyword not actually operate on suspend, but Kotlin provides it?
Because it’s not meant to handle suspension.
The suspended operation — that is, thread cutting — relies on the actual code inside the suspended function, not on this keyword.
So this keyword, it’s just a reminder.
Furthermore, when you define the suspend function without the suspend logic, you are reminded of the redundant suspend modifier, which tells you that suspend is redundant.
So it makes sense to create a suspend function that calls Kotlin’s native suspend function, either directly or indirectly, in order for it to contain true suspend logic.
You can customize the suspend function by writing it as long as it is time consuming
After learning the suspension of coroutines, there is still a concept of confusion, that is, the non-blocking suspension of coroutines. What is the non-blocking
Non-blocking suspend
Nonblocking is relative to blocking
Blocking type is very easy to understand, a road traffic jam, the front vehicles do not start, behind all vehicles are blocked, behind the car want to pass, or wait for the front car to leave, or open up a road, drive away from the new road
This is similar to threads in code:
The path is blocked – the time-consuming task leaves and the previous car leaves – the time-consuming task ends and starts a new path – The thread is switched to another thread
Semantically speaking, non-blocking suspension is one of the characteristics of the suspension, the suspension of coroutines is non-blocking, nothing else is expressed
Nature of blocking
First of all, all code is blocking in nature, and only time-consuming code can cause human perceivable wait. For example, a 50 ms operation on the main thread will cause the interface to block several frames, which can be observed by our human eyes, and this is commonly referred to as “blocking”.
For example, when you develop an app that runs well on a good phone and freezes on a bad old phone, you’re saying the same line of code doesn’t take the same time.
In the video, there is an example of network IO. IO blocking is mostly reflected in “waiting”. Its performance bottleneck is data exchange with the network.
And this has nothing to do with coroutines, cutting threads can’t solve things, coroutines can’t solve.
So, to summarize the coroutines
- Coroutines are cutting threads
- A hang is a cut thread that can be cut back automatically
- Non-blocking is the implementation of non-blocking operations using code that appears to block
Coroutines don’t create anything new, they just make multithreaded development easier, again by switching threads and calling back to the original thread
Advanced use of coroutines
launch
withasync
Let’s compare launch and Async
The usage is similar, both can start a coroutine
launch
Start a new coroutine without returning a result. Any job deemed “once and for all” can be usedlaunch
To start theasync
A new coroutine is launched with a name namedawait
Hangs the function and returns the result later
For example, if we want to display a list of data sources from two interfaces, if we launch the coroutine with launch, we will launch two requests. When either request ends, we will check the results of the other request, and wait for the two requests to end and start merging the data sources for display
If we use async
val listOne = async { fetchList(1)}val listTwo = async { fetchList(2) } mergeList(listOne.await(), listTwo.await())// mergeList is a custom merge function
Copy the code
By calling await() on each deferred reference, we can guarantee that the two async items will be merged after completion without any precedence considerations
You can also use awaitAll() for collections
val deferreds = listOf( async { fetchList(1)}, async { fetchList(2)} ) mergeList(deferreds.awaitAll())
Copy the code
Normally, you just need to launch the coroutine with launch, but when using async, note that async expects you to eventually call await to get the result (or exception), so it does not throw an exception by default.
Dispatchers
Kotlin provides three dispatchers that can be used for thread scheduling.
a | b |
---|---|
Dispatchers.Main | Android main thread, used to interact with users |
Dispatchers.IO | Suitable for IO intensive tasks such as reading and writing files, operating databases, and network requests |
Dispatchers.Default | Optimized for CPU intensive work such as computation /JSON parsing, etc |
Exception propagation and handling
Job
andSupervisorJob
In general, when we create coroutines using launch or async, the default creation will be handled by Job. If a task fails, it will affect its child coroutines and parent coroutines. As is shown in
The exception will reach the root of the hierarchy, and any coroutines currently started by the CoroutineScope will be cancelled.
If we don’t want the failure of one task to affect other tasks, and the child coroutine running failure doesn’t affect the other child coroutines or the parent coroutine, we can use another extension of the Job in the CoroutineContext of the CoroutineScope when we create the coroutine: the SupervisorJob
When a child coroutine task goes wrong or fails, it’s not going to be able to cancel it and its own children, or broadcast the exception to its parent, it’s going to let the child handle the exception itself
coroutineScope
和supervisorScope
With launch and Async, it is easy to start a thread, request the network and fetch data
Sometimes, however, your requirements are more complex and you need to execute multiple network requests in a single coroutine, which means you need to start more coroutines.
To create more coroutines in the hang function, you can use a builder called the coroutineScope or supervisorScope to launch more coroutines.
suspend fun fetchTwoDocs(a) { coroutineScope { launch { fetchList(1) } async { fetchList(2)}}}Copy the code
Note: CoroutineScope and coroutineScope are different things, even though their names are only one character different. CoroutineScope is a coroutineScope, and coroutineScope is a suspend function that creates a new coroutine in a suspend function, It takes the CoroutineScope as a parameter and creates coroutines in the CoroutineScope
What’s the main difference between the coroutineScope and the container? It’s what happens when the subcoroutine goes wrong
When the coroutineScope is a context-creation scope that inherits an external Job, its internal cancel operations are propagated in both directions, and exceptions not caught by the child coroutine are also passed up to the parent coroutine. If any of the subcoroutines exits abnormally, the whole thing exits.
The container is also designed to inherit the context of the external scope, but the internal cancellations are propagated one way, with the parent propagating to the child coroutine and the other way around, meaning that an exception from the child coroutine doesn’t affect the parent coroutine or its siblings.
So when dealing with multiple concurrent tasks, it’s safe to run the container with the coroutine it’s designed to be able to create while the failure of one is small and it’s safe to run the container with the coroutineScope
Note: the container is only designed to work as described above when it is part of the container that is the container’s container or CoroutineScope(Container).
Coroutine exception handling
Exceptions to coroutines are handled using either a try/catch or a runCatching built-in function (which also uses try/catch internally). The request code is written in a try, and the catch catches the exception.
For example,
GlobalScope.launch { val scope = CoroutineScope(Job()) scope.launch { try { throw Exception("Failed")}catch (e: Exception) { // Catch an exception}}}
Copy the code
Normally, only code blocks in a try-catch block have exceptions, which are caught in the catch. But there are special cases of exceptions in coroutines.
For example, if a failed child coroutine is opened in a coroutine, it cannot be caught. Again, the above example:
GlobalScope.launch { val scope = CoroutineScope(Job()) try { Scope. Launch {throw Exception("Failed")}} catch (e: Exception) {e.printStackTrace() // Failed to catch exceptions, program crashes}}
Copy the code
We create a child coroutine in a try-catch block that throws an exception. We expect the exception to be caught in the catch, but when we run it, our App crashes and exits. This also validates that try-catch is invalid.
This involves the problem of exception propagation in coroutines
Exception propagation
In Kotlin’s coroutines, each coroutine is a scope, and the newly created coroutine has a hierarchy with its parent scope. And this cascading relationship mainly lies in:
If a task in a coroutine fails due to an exception, it immediately passes the exception to its parent, who decides to handle it:
- Cancels its own children;
- Cancel itself;
- Propagates the exception and passes it to its parent
That’s why our try-catch child coroutine fails, because an exception propagates up the child, but the parent task doesn’t handle the exception, causing the parent task to fail.
If you modify the above example again:
GlobalScope.launch { val scope = CoroutineScope(Job()) val job = scope.async { Async throw Exception("Failed")} try {job.await()} catch (e: Exception) {e.printStackTrace()}}
Copy the code
Why does async use try-catch to catch exceptions? An exception is thrown when calling **.await() ** when async is used as the root coroutine. The root coroutine here refers to the coroutine instance of the CoroutineScope(container container) or the direct child of the container container container
So a try-catch wrapper.await() can catch an exception
If async is not used as a root coroutine, for example:
val scope = CoroutineScope(Job()) scope.launch { Val job = async {//async start child coroutine throw Exception("Failed")} try {job.await()} catch (e: Exception) {e.printStackTrace() // Failed to catch exceptions, program crashes}}
Copy the code
The program crashes because launch is used as the root coroutine, and exceptions from child coroutines must be propagated to the parent coroutine. No matter whether the child coroutine is Launch or Async, exceptions will not be thrown, so it cannot be caught
If the exceptions generated by the child coroutines async creates are not passed up, can we avoid the exception affecting the parent coroutine and causing the application to crash?
val scope = CoroutineScope(Job()) scope.launch { supervisorScope { Val job = async {//async = throw Exception("Failed")} try {job. Await ()} catch (e: Exception) {e.printStackTrace()}}}
Copy the code
or
val scope = CoroutineScope(Job()) scope.launch { coroutineScope { val job = async(SupervisorJob()) { //async start child coroutine throw Exception("Failed")} try {job.await()} catch (e: Exception) {e.printStackTrace()}}}
Copy the code
In fact, the two examples above, which use the container container container container scope and the container container CoroutineScope(container Job), respectively, are designed to handle exceptions that are not passed upwards and are instead thrown by the current coroutine, try-catch
The container is designed to handle anomalies that are not being handled by the container without the use of the CoroutineScope(container container), which is known as the root container, and is being passed up to the root, causing the parent to fail.
CoroutineExceptionHandler
Coroutines handle exceptions of the second method is to use CoroutineExceptionHandler
For throwing coroutines, automatically created (launch coroutines) an uncaught exception, we can use CoroutineExceptionHandler to deal with
CoroutineExceptionHandler is used for global “catch all” finally a mechanism of behavior. You can’t recover from abnormal in CoroutineExceptionHandler. When the handler is called, the coroutine has already completed the corresponding exception. Typically, this handler is used to log exceptions, display some kind of error message, and terminate and/or restart the application.
This passage is a little hard to understand, read alternatively understand CoroutineExceptionHandler is the manner in which the global catch exceptions, explain exceptions the child scope themselves up, to reach the top of the scope, prove scope are all cancelled, CoroutineExceptionHandler is called, all child coroutines has delivered the corresponding anomaly, there would be no new exception passed
So CoroutineExceptionHandler must be set at the top of the scope to catch exceptions, or failed to capture.
The use of CoroutineExceptionHandler
Here’s how to declare a CoroutineExceptionHandler example.
val exHandler = CoroutineExceptionHandler{context, exception -> println(exception) } val scope = CoroutineScope(Job()) scope.launch { launch(exHandler) { throw Exception("Failed") // Exception catch failed}}
Copy the code
The reason exceptions are not caught is because exHandler is not given to the parent. The internal coroutine propagates an exception as it occurs and passes it to its parent, which is unaware of the handler’s existence and the exception is not thrown.
Change to the following example to catch the exception normally
val exHandler = CoroutineExceptionHandler{context, exception -> println(exception) } val scope = CoroutineScope(Job()) scope.launch(exHandler) {Launch {throw Exception("Failed")}}
Copy the code
The shortage of the CoroutineExceptionHandler
-
Since there is no try-catch to catch an exception, the exception propagates upwards until it reaches the root coroutine. Due to the structured concurrency nature of the coroutine, when the exception propagates upwards, the parent coroutine will fail, as will the cascading child coroutines and their siblings.
-
CoroutineExceptionHandler role in global catch exceptions, CoroutineExceptionHandler couldn’t exception handling in the particular section of the code, for example, to a single interface failure, unable to retry or other specific operation after the exception.
-
Try-catch is better if you want to do exception handling in a particular part.
conclusion
The exception catching mechanism of coroutine mainly consists of two points: local exception catching and global exception catching
Scope of exception occurrence:
-
Scope, try-catch directly, you can directly catch exceptions for processing
-
scope
-
The scope launched by launch cannot catch exceptions and is immediately passed in both directions and eventually thrown
-
Scope of async startup:
- if
async
inCoroutineScope(SupervisorJob)
Instance orsupervisorScope
Start the coroutine, the exception will not be passed up, can be inasync.await()
Time catch exception - if
async
In theSupervisorJob
Instance orsupervisorScope
Is started in the direct subcoroutine, then the exception propagates bidirectionally inasync.await()
Cannot catch an exception
- if
-
It’s unusual in the container, it’s not going up, it’s only going to affect itself
CoroutineScope exceptions will be passed in both directions, affecting itself and its parent
CoroutineExceptionHandler can only capture the launch of the anomaly, launch the exception will be immediately passed to the parent, and CoroutineExceptionHandler must give the top launch will only take effect
Reference documentation
Kotlin coroutines on Android
Coroutines on Android (part I): Getting the background
A hard glance at Kotlin’s coroutines. – Can’t you learn coroutines? Probably because the tutorials you read are all wrong
Exceptions in coroutines
What happens when a problem occurs in the schedule? – Coroutine exception
How exactly does the Kotlin coroutine handle exceptions? Teach you a variety of options!