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 passcontextParameters to manage and control the coroutine lifecycle (herecontextIt’s not the same thing as Android, it’s a more general concept, and there will be an Android platform wrapper to work with),CoroutineScopeTo create the scope of the coroutine
  • Methods threeGlobalScopeisCoroutineScopeSubclass, use scenarios are not elegant.
  • The difference between method four and method three islaunchandasyncThis 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

launchwithasync

Let’s compare launch and Async

The usage is similar, both can start a coroutine

  • launchStart a new coroutine without returning a result. Any job deemed “once and for all” can be usedlaunchTo start the
  • asyncA new coroutine is launched with a name namedawaitHangs 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

JobandSupervisorJob

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

coroutineScopesupervisorScope

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:

      • ifasyncinCoroutineScope(SupervisorJob)Instance orsupervisorScopeStart the coroutine, the exception will not be passed up, can be inasync.await()Time catch exception
      • ifasyncIn theSupervisorJobInstance orsupervisorScopeIs started in the direct subcoroutine, then the exception propagates bidirectionally inasync.await()Cannot catch an exception

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!