One of Kotlin Coroutine actual combat

What problems does the Android collaboration solve?

  • Handle time-consuming tasks that often block the main thread
  • Ensure that the main thread is safe and that any suspend function is safely called from the main thread

Example (asynchronous task)

Implement an example of requesting network data: there is a button, a loading and a textview on the page to display the results. Click button to display loading, send a request to the server, get the data, display the result on the textview, and hide loading.

The interface is as follows

Service and Retrofit

getUsers() is called by AsyncTask, and getUserList() is a suspend method called by coroutine.

  • How to use AsyncTask

    private fun doWithAsyncTask() {
      val task = object : AsyncTask<Void, Void, List<User>>() {
          override fun onPreExecute() {
              super.onPreExecute()
              showLoading()
          }
    
          override fun doInBackground(vararg p0: Void?): List<User>? {
              return userDataPoint.getUsers().execute().body()
          }
    
          override fun onPostExecute(result: List<User>?) {
              super.onPostExecute(result)
              Log.d(TAG, "onPostExecute: done.")
              showData(result)
          }
      }
      task.execute()
    }

Using a coroutine, call the suspend function suspend

private fun doWithCoroutine() {
    GlobalScope.launch(Dispatchers.Main) {
        showLoading()
        val users = withContext(Dispatchers.IO) {
            userDataPoint.getUserList()
        }
        showData(users)
    }
}

Let's learn more about the collaborative process and its benefits.

What is a collaborative process?

Coprocessing synchronizes asynchronous logic and eliminates callback hell.
The core of a coroutine is that a function or a program can be suspended and restored at the suspended position later.

Suspension and recovery of collaboration

  • The basic operations of regular functions include invoke (or call) and return. suspend and resume are added to the coroutine

    • Suspend, also known as suspend or suspend, is used to suspend the execution of the current coroutine and save all local variables.
    • Resume is used to resume the execution of a suspended process from its suspended position.

    Suspend function

  • A function decorated with the suspend keyword is called a suspend function
  • Suspended functions can only be called within a coroutine body or other suspended functions

    Difference between hang and block

    Case comparison: delay(5000) in the coroutine is a suspend function, and thread in Java Sleep (5000) is a blocking function.
    For example, in the click event, calling the delay function in the association will not block the main thread and what the main thread should do. However, using sleep will block the main thread. The main thread is stuck there and waits for 5 seconds to continue.

    Two parts of the collaborative process

  • The infrastructure layer, the standard library's collaboration API, mainly provides the most basic conceptual and semantic support for collaboration.
  • The business framework layer is supported by the upper framework of the collaboration process. GlobalScope and delay functions all belong to this layer.
    Using the infrastructure layer to create a collaboration process will be a lot of trouble, so the business framework layer is mostly used in development.
    For example, the relationship between NIO and Netty. NIO is the basic API and Netty is the business framework based on NIO. Take a look at a collaboration created using the infrastructure layer and its implementation.

    // Create a process body
    val continuation = suspend {
      userDataPoint.getUserList()
    }.createCoroutine(object: Continuation<List<User>>{
    
      override val context: CoroutineContext = EmptyCoroutineContext
    
      override fun resumeWith(result: Result<List<User>>) {
          Log.d(TAG, "resumeWith: ${result.getOrNull()}")
      }
    })
    // Start process
    continuation.resume(Unit)

    In fact, it's still callback...
    In the above example, the basic framework uses kotlin The API under the coroutines package, and the business framework layer uses kotlinx API under coroutines package

    Scheduler

    All coroutines must run in the scheduler, even if they run on the main thread.

  • Dispatchers.Main, the main thread on Android, handles UI interaction and some lightweight tasks

    • Call suspend function
    • Call UI function
    • Update LiveData
  • Dispatchers.IO, disk and network IO

    • database
    • File reading and writing
    • network processing
  • Dispatchers.Default, non main thread, CPU intensive task

    • Array sorting
    • JSON parsing
    • Processing difference judgment

    Task disclosure

  • When a collaborative task is lost and cannot be tracked, it will waste resources such as memory, CPU and disk, and even send a useless network request. This situation is called task leakage.

    • For example, click the button to send a request to the server. Before the server has finished processing, press the return key to exit the application. The Activity has been destroyed. At this time, the network request is still in progress. The network request object still occupies memory, and task leakage will occur.
  • In order to avoid the disclosure of collaborative tasks, Kotlin introduces a structured concurrency mechanism.

    Structured concurrency

    This can be done using structured concurrency

  • Cancel a task. Cancel a task when it is no longer needed.
  • Track a task. When a task is executing, track it.
  • Send an error signal. When the collaboration fails, send an error signal to indicate that an error has occurred.

    Coroutine scope

    Defining a collaboration must specify its CoroutineScope, which will track all collaborations and cancel all collaborations started by it.
    Common API s are:

  • For GlobalScope, the life cycle is process level. Even if the Activity/Fragment is destroyed, the collaboration process is still executing.
  • MainScope, used in Activity, can cancel a collaboration in onDestroy.
  • viewModelScope, which can only be used in ViewModel, binds the lifecycle of ViewModel.
  • lifecycleScope, which can only be used in Activity/Fragment, binds the life cycle of Activity/Fragment.

MainScope:

private val mainScope = MainScope()
private fun doWithMainScope() {
    mainScope.launch {
        try {
            showLoading()
            val users = userDataPoint.getUserList()
            showData(users)
        } catch (e: CancellationException) {
            Log.d(TAG, "CancellationException: $e")
            showData(emptyList())
        }
    }
}
override fun onDestroy() {
    super.onDestroy()
    mainScope.cancel()
}

In the callback of Activity destruction, canceling mainScope will cancel the tasks inside and throw a cancelationexception. You can handle the cancelled operation in catch.
In addition, MainScope is CoroutineScope. CoroutineScope is an interface that allows an activity to implement the interface and use MainScope as the delegate object by default. In this way, you can directly use launch and cancel in the activity.

class MainActivity : AppCompatActivity(), CoroutineScope by MainScope()
//private val mainScope = MainScope()
private fun doWithMainScope() {
    launch {
        showLoading()
        try {
            val users = userDataPoint.getUserList()
            showData(users)
        } catch (e: CancellationException) {
            Log.d(TAG, "CancellationException: $e")
            showData(emptyList())
        }
    }
}
override fun onDestroy() {
    super.onDestroy()
    cancel()
}

Startup and cancellation of collaboration

  • Start process

    • Start builder
    • Startup mode
    • Scope builder
    • Job lifecycle
  • Cancel collaboration

    • Cancellation of collaboration
    • CPU intensive task cancellation
    • Side effects of collaborative process cancellation
    • Timeout task

    Collaborative process builder

    Both the launch and async builders are used to start new processes

  • launch, which returns a Job without any results
  • async returns a Deferred, which is also a Job and can be used await() gets its final result on a Deferred value.
    Waiting for a job
  • join and await. Both are suspended functions and do not block the main thread.
  • Combination development

    Example 1: launch and async start the process

    GlobalScope is a top-level collaboration and is not recommended. runBlocking can wrap the test method into a co process, and the test method runs on the main thread. In a collaboration, you can start the collaboration with launch and async.

    @Test
    fun test_coroutine_builder() = runBlocking {
      val job1 = launch {
          delay(2000)
          println("job1 finished.")
      }
    
      val job2 = async {
          delay(2000)
          println("job2 finished")
          "job2 result"
      }
      println("job1: $job1")
      println("job2: $job2")
      println("job2: ${job2.await()}")
    }

    Output results

    job1: "coroutine#2":StandaloneCoroutine{Active}@71b1176b
    job2: "coroutine#3":DeferredCoroutine{Active}@6193932a
    job1 finished.
    job2 finished
    job2: job2 result

    Both builders can start the co process. As above, await can get the result returned by the async builder. job1 and job2 are sub processes in the main process of runBlocking packaging. After the two sub processes are executed, the main process will exit.

    Coordination execution sequence

    @Test
    fun test_join() = runBlocking {
      val start = System.currentTimeMillis()
      println("start: ${System.currentTimeMillis() - start}")
      val job1 = launch {
          delay(2000)
          println("job1, ${System.currentTimeMillis() - start}")
      }
      val job2 = launch {
          delay(1000)
          println("job2, ${System.currentTimeMillis() - start}")
      }
      val job3 = launch {
          delay(5000)
          println("job3, ${System.currentTimeMillis() - start}")
      }
      println("end: ${System.currentTimeMillis() - start}")
    }

    Start three cooperative processes with launch, delaying 2 seconds in job1, 1 second in job2 and 5 seconds in job3. Execution result output

    start: 0
    end: 8
    job2, 1018
    job1, 2017
    job3, 5018

    Start and end swish and finish printing in the main cooperative process. In the middle, start three cooperative processes with the launch starter for 8 milliseconds. job2 prints after 1 second, job1 prints after 2 seconds, and job3 prints after 5 seconds.

    Example 2: join

    If you want to control their order, use the join function:

    @Test
    fun test_join() = runBlocking {
      val start = System.currentTimeMillis()
      println("start: ${System.currentTimeMillis() - start}")
      val job1 = launch {
          delay(2000)
          println("job1, ${System.currentTimeMillis() - start}")
      }
      job1.join()
      val job2 = launch {
          delay(1000)
          println("job2, ${System.currentTimeMillis() - start}")
      }
      job2.join()
      val job3 = launch {
          delay(5000)
          println("job3, ${System.currentTimeMillis() - start}")
      }
      println("end: ${System.currentTimeMillis() - start}")
    }

    The results are as follows:

    start: 0
    job1, 2016
    job2, 3022
    end: 3024
    job3, 8025

    start, then execute job1 and print job1. At this time, 2016ms has passed because the task in job1 has been delay ed for 2s.
    Then execute job2 and print job2. Because the task in job2 is delay ed for 1s, the time at this time passes about 3s.
    The join function is not used for job3, so end is printed directly. After another 5 seconds, job3 delay is completed, and job3 is printed.
    If you call the join() function on job3 before end, the result is as follows:

    start: 0
    job1, 2014
    job2, 3018
    job3, 8019
    end: 8019

    end prints after job3 is executed.

    Example 3: await

    Test await:

    fun test_await() = runBlocking {
      val start = System.currentTimeMillis()
      println("start: ${System.currentTimeMillis() - start}")
      val job1 = async {
          delay(2000)
          println("job1, ${System.currentTimeMillis() - start}")
          "result 1"
      }
      println(job1.await())
      val job2 = async {
          delay(1000)
          println("job2, ${System.currentTimeMillis() - start}")
          "result 2"
      }
      println(job2.await())
      val job3 = async {
          delay(5000)
          println("job3, ${System.currentTimeMillis() - start}")
          "result 3"
      }
      println(job3.await())
      println("end: ${System.currentTimeMillis() - start}")
    }

    Output results

    start: 0
    job1, 2018
    result 1
    job2, 3027
    result 2
    job3, 8032
    result 3
    end: 8033

    await also allows the sub processes to execute in order and returns the results after the process is executed.

    combination

    @Test
    fun test_sync() = runBlocking {
      val time = measureTimeMillis {
          val one = firstTask()
          val two = secondTask()
          println("result: ${one + two}")
      }
      println("total time: $time ms")
    }
    
    private suspend fun firstTask(): Int {
      delay(2000)
      return 3
    }
    
    private suspend fun secondTask(): Int {
      delay(2000)
      return 6
    }

    Two tasks, each delayed for 2 seconds, and then returned a value; In the test function, use measureTimeMillis in the main collaboration to calculate the time consumption of the code block, then print it, get the return values of the two tasks, and then add and output the results

    result: 9
    total time: 4010 ms

    It takes more than 4 seconds in total. The second task takes 2 seconds after the first task is executed.

    Example 4: using async

    Use async to execute two tasks at the same time, and use await to obtain the returned results. See an example:

    @Test
    fun test_combine_async() = runBlocking {
      val time = measureTimeMillis {
          val one = async { firstTask() }
          val two = async { secondTask() }
          println("result: ${one.await() + two.await()}")
      }
      println("total time: $time ms")
    }

    give the result as follows

    result: 9
    total time: 2025 ms

    It takes about 2 seconds in total, and the two tasks are executed at the same time.
    Use the above example to sort out the process again. runBlocking ensures that it is the main collaboration started in the main thread. Then, in line 4, it starts the collaboration one in the main collaboration to execute the task firstTask. In line 5, it starts the collaboration two in the main collaboration to execute the task secondTask. The total tasks of the two subprocesses, one and two, are executed concurrently. In line 6, wait for both one and two to return results, Add the two and output the result.
    Look at the following wording:

    @Test
    fun test_combine_async_case2() = runBlocking {
      val time = measureTimeMillis {
          val one = async { firstTask() }.await()
          val two = async { secondTask() }.await()
          println("result: ${one + two}")
      }
      println("total time: $time ms")
    }

    In line 4, start the co process one and wait for the result, which takes about 2 seconds; Then start the co process two in line 5 and wait for the result. It takes about 2 seconds. Calculate in line 6 and output the result. It took more than four seconds. If you want parallel effects, this is wrong.

    result: 9
    total time: 4018 ms

    Startup mode of collaborative process

    public fun CoroutineScope.launch(
      //Context fetcher
      context: CoroutineContext = EmptyCoroutineContext,
      //Startup mode
      start: CoroutineStart = CoroutineStart.DEFAULT,
      //Co process method
      block: suspend CoroutineScope.() -> Unit
    ): Job {
      val newContext = newCoroutineContext(context)
      val coroutine = if (start.isLazy)
          LazyStandaloneCoroutine(newContext, block) else
          StandaloneCoroutine(newContext, active = true)
      coroutine.start(start, coroutine, block)
      return coroutine
    }

    In the construction method, the second parameter is the startup mode, which defaults to DEFAULT. There are four modes.

  • DEFAULT: start scheduling immediately after the collaboration is created. If the collaboration is cancelled before scheduling, it will directly enter the status of cancellation response.
  • ATOMIC: scheduling starts immediately after the collaboration is created. The collaboration will not respond to cancellation until it reaches the first hanging point.
  • LAZY: scheduling will be started only when the collaboration is needed, including actively calling the start, join or await functions of the collaboration. If the scheduling is cancelled before scheduling, the collaboration will directly enter the abnormal end state.
  • UNDISPATCHED: the coroutine is executed in the current function call stack immediately after it is created until the first real hang point is encountered.
    Note: scheduling does not represent execution.
    It can be understood that there is a queue. Scheduling is to add the tasks of the collaboration process to the queue, which does not mean execution.
    There is a time interval from scheduling to execution; A collaboration can be cancelled.

    public enum class CoroutineStart {
      //After the collaboration is created. Start scheduling now. If the collaboration is cancelled before scheduling. Directly enter the state of canceling the response
      DEFAULT,
      //When we need him to execute, we will execute, otherwise we won't execute.
      LAZY,
      //Start scheduling immediately. The cancellation will not be responded to before the first hang point
      ATOMIC,
      //After creating a coroutine, it is executed in the call stack of the current function. Until the first hanging point is encountered.
      UNDISPATCHED
    }

    DEFAULT

    Start scheduling immediately after the collaboration is created. If it is cancelled before scheduling, it will directly enter the state of cancellation response.

Start scheduling immediately after a collaboration is created: it means that after a collaboration is created, it is added to the scheduler, that is, the queue mentioned above. The task will trigger itself without start() or join(). The following test code shows that start () has no effect on the test results.
If it is cancelled before scheduling, it will directly enter the cancellation response state: This is more interesting. As mentioned earlier, creating a collaboration will be added to the queue and the task will trigger itself. It is difficult to control the time of canceling. Is it before or after the task is executed? Even if you cancel immediately after the collaboration is created, it is possible that the scheduler is already executing the code block of the collaboration.

@Test
fun test_fib_time() {
    val time = measureTimeMillis {
        println("result: ${fib(45)}")
    }
    println("time used: $time")
}
private fun fib(n: Int): Long {
    if (n == 1 || n == 2) return 1L
    return fib(n - 1) + fib(n - 2)
}

This is a code for calculating Fibonacci sequence. It takes about 3 seconds to calculate 45.

result: 1134903170
time used: 3132

Let's look at the test code. Use DEFAULT to start the process. fib(46) is calculated in the process. This is a time-consuming operation > 3S:

@Test
fun test_default() {
    val start = System.currentTimeMillis()
    val job = GlobalScope.launch(start = CoroutineStart.DEFAULT) {
        println("Job started. ${System.currentTimeMillis() - start}")
        val res = fib(46)
        println("result: $res")
        println("Job finished. ${System.currentTimeMillis() - start}")
    }
    job.cancel()
    println("end.")
}

Two results can be obtained by running several more times.
Result 1 is as follows:

end.
Job started. 124

Result 2 is as follows:

end.

Result 1: after the job was created, it started scheduling immediately and was added to the queue. Without calling the start() or join() methods, the task triggered itself and was cancel led when executing time-consuming operations.
Result 2: after the job was created, it started scheduling immediately and was added to the queue. It was cancel led before it could be triggered by itself.
Therefore, it is difficult to grasp the time of cancel. Cancel immediately after creation, and the scheduler may or may not have time to execute the task. And calling start(),join(), has no effect. You can call start() before cancel, and the effect is the same.

LAZY

Only execute when collaborative process execution is required, otherwise it will not be executed. If it is cancelled before scheduling, it will directly enter the abnormal end state.

It can only be executed when it needs to be executed, that is, it can only be started when start() or join() is executed. Whether to execute the co process depends on whether to start().
If it is cancelled before scheduling, it will directly enter the abnormal end state, that is, add the collaboration task to the scheduler and wait for execution. If the collaboration is cancelled at this time, it will not be executed.

@Test
fun test_lazy() {
    val start = System.currentTimeMillis()
    val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
        println("Job started. ${System.currentTimeMillis() - start}")
        val res = fib(46)
        println("result: $res")
        println("Job finished. ${System.currentTimeMillis() - start}")
    }
    job.cancel()
    println("end.")
}

Using LAZY to start the process, the test results will always be output
end.
Because without start, it will never be executed.

However, adding start() does not guarantee that it will be executed. for example

@Test
fun test_lazy() {
    val start = System.currentTimeMillis()
    val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
        println("Job started. ${System.currentTimeMillis() - start}")
        val res = fib(46)
        println("result: $res")
        println("Job finished. ${System.currentTimeMillis() - start}")
    }
    job.start()
    job.cancel()
    println("end.")
}

Line 10 adds start(), but the result may be execution or it may end before execution.
Change the position of lines 10 and 11 and cancel first. At start, you can never enter execution.

ATOMIC

After the collaboration process is created, it starts scheduling immediately. The collaboration process will not respond to cancellation until it reaches the first hanging point.

@Test
fun test_atomic() = runBlocking {
    val start = System.currentTimeMillis()
    val job = launch(start = CoroutineStart.ATOMIC) {
        println("Job started. ${System.currentTimeMillis() - start}")
        val result = fib(46)
        println("result $result")
        delay(1000)
        println("Job finished. ${System.currentTimeMillis() - start}")
    }
    job.cancel()
    println("end.")
}

result:

end.
Job started. 9
result 1836311903

Process finished with exit code 0

The first hanging point is the delay hanging function on line 8, so the Job started is printed after the Job is executed. Although it is cancelled, it will not respond to cancellation until the first hanging point is executed. It will not respond to cancel until the fib function result is printed for about 5 seconds.
In applications, operations that cannot be cancelled are usually placed before the pending function, and the cancellation is responded only after its execution is completed.

UNDISPATCHED

After the coprocessor is created, it is executed in the current function call stack immediately until the first real hang point is encountered.

@Test
fun test_un_dispatched() = runBlocking {
    val start = System.currentTimeMillis()
    val job = launch(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
        println("Job started. ${Thread.currentThread().name}, ${System.currentTimeMillis() - start}")
        val result = fib(46)
        println("result $result ${System.currentTimeMillis() - start}")
        delay(1000)
        println("Job finished. ${System.currentTimeMillis() - start}")
    }
    job.cancel()
    println("end.")
}

result

Job started. main @coroutine#2, 3
result 1836311903 5073
end.

Process finished with exit code 0

Specify context = dispatchers when starting a collaboration IO is started in undispatched mode. During the execution of the collaboration, the current thread name is printed. It is the main thread, not the IO thread. Because runBlocking is running on the main thread, that is, the current function call stack.

Scope builder for a collaboration

coroutineScope and runBlocking

runBlocking is a regular function, which is mainly used for main functions and tests, while coroutineScope is a suspended function
They will all wait for the end of their coroutine body and all child coroutines. The main difference is that the runBlocking method will block the current thread to wait, while coroutineScope is only suspended and will release the underlying thread for other purposes.
runBlocking example

@Test
fun test_coroutine_scope_runBlocking() {
    val start = System.currentTimeMillis()
    runBlocking {
        val job1 = async {
            println("job 1 start. ${System.currentTimeMillis() - start}")
            delay(400)
            println("job 1 end. ${System.currentTimeMillis() - start}")
        }
        val job2 = launch {
            println("job 2 start. ${System.currentTimeMillis() - start}")
            delay(200)
            println("job 2 end. ${System.currentTimeMillis() - start}")
        }
        println("runBlocking end.")
    }
    println("Program end.")
}

Output results

runBlocking end.
job 1 start. 121
job 2 start. 127
job 2 end. 333
job 1 end. 525
 Program end.
Process finished with exit code 0

Before the program ends, it will wait for the end of the subprocess in runBlocking.

Examples of coroutineScope

@Test
fun test_coroutine_scope_coroutineScope() = runBlocking {
    val start = System.currentTimeMillis()
    coroutineScope {
        val job1 = async {
            println("job 1 start. ${System.currentTimeMillis() - start}")
            delay(400)
            println("job 1 end. ${System.currentTimeMillis() - start}")
        }
        val job2 = launch {
            println("job 2 start. ${System.currentTimeMillis() - start}")
            delay(200)
            println("job 2 end. ${System.currentTimeMillis() - start}")
        }
        println("coroutineScope end.")
    }
    println("Program end.")
}

Output results

coroutineScope end.
job 1 start. 16
job 2 start. 28
job 2 end. 233
job 1 end. 424
 Program end.
Process finished with exit code 0

Similarly, it will wait for the sub processes in coroutineScope to end before the program ends. As mentioned above, coroutineScope is a suspended function that needs to be executed in the coroutine. Therefore, runBlocking is used to generate a coroutine environment for coroutineScope to run.

coroutineScope and supervisorScope

  • Coroutine scope: if a collaboration fails, other sibling collaborations will also be cancelled.
  • supervisorScope: if a collaboration fails, it will not affect other sibling collaborations.
    For example, the test with coroutineScope failed

    @Test
    fun test_coroutine_scope_coroutineScopeFail() = runBlocking {
      val start = System.currentTimeMillis()
      coroutineScope {
          val job1 = async {
              println("job 1 start. ${System.currentTimeMillis() - start}")
              delay(400)
              println("job 1 end. ${System.currentTimeMillis() - start}")
          }
          val job2 = launch {
              println("job 2 start. ${System.currentTimeMillis() - start}")
              delay(200)
              throw IllegalArgumentException("exception happened in job 2")
              println("job 2 end. ${System.currentTimeMillis() - start}")
          }
          println("coroutineScope end.")
      }
      println("Program end.")
    }

    result

    coroutineScope end.
    job 1 start. 15
    job 2 start. 24
    java.lang.IllegalArgumentException: exception happened in job 2
    ...
    
    Process finished with exit code 255

    Job1 and job2 are two brother processes started in coroutine scope. Job2 fails and job1 does not continue to execute.
    For example, if the test with supervisor scope fails, replace the coroutine scope above

    supervisorScope end.
    job 1 start. 10
    job 2 start. 17
    Exception in thread "main @coroutine#3" java.lang.IllegalArgumentException: exception happened in job 2
    ...
    job 1 end. 414
     Program end.
    Process finished with exit code 0

    Job1 and job2 are two sibling processes started in supervisor scope. If job2 fails and throws an exception, job1 continues to execute until the task is completed.

    Job object

    For each created collaboration (through launch or async), a Job instance will be returned, which is the unique identifier of the collaboration and is responsible for managing the lifecycle of the collaboration.
    A Job can contain a series of states. Although these states cannot be accessed directly, the properties of the Job (isActive, isCancelled, isCompleted) can be accessed

  • Create New
  • Active
  • Completing
  • Completed
  • Canceling
  • Cancelled

    @Test
    fun test_job_status() = runBlocking {
      val start = System.currentTimeMillis()
      var job1: Job? = null
      var job2: Job? = null
      job1 = async {
          println("job 1 start. ${System.currentTimeMillis() - start}")
          delay(400)
          job1?.let { println("Job1- isActive:${it.isActive}, isCancelled:${it.isCancelled}, isCompleted:${it.isCompleted}") }
          job2?.let { println("Job2- isActive:${it.isActive}, isCancelled:${it.isCancelled}, isCompleted:${it.isCompleted}") }
          println("job 1 end. ${System.currentTimeMillis() - start}")
      }
      job2 = launch {
          println("job 2 start. ${System.currentTimeMillis() - start}")
          delay(200)
          job1?.let { println("Job1- isActive:${it.isActive}, isCancelled:${it.isCancelled}, isCompleted:${it.isCompleted}") }
          job2?.let { println("Job2- isActive:${it.isActive}, isCancelled:${it.isCancelled}, isCompleted:${it.isCompleted}") }
          println("job 2 end. ${System.currentTimeMillis() - start}")
      }
      println("Program end.")
    }

    Output:

    Program end.
    job 1 start. 8
    job 2 start. 15
    Job1- isActive:true, isCancelled:false, isCompleted:false
    Job2- isActive:true, isCancelled:false, isCompleted:false
    job 2 end. 216
    Job1- isActive:true, isCancelled:false, isCompleted:false
    Job2- isActive:false, isCancelled:false, isCompleted:true
    job 1 end. 416
    Process finished with exit code 0

    After the execution of job2, the information of job2 is printed in job1. Since job2 has been completed, isActive=false, isCompleted=true
    Look at the picture below

    Cancellation of collaboration

  • Canceling a scope cancels its children
  • The cancelled subprocess will not affect the other sibling subprocesses
  • The coroutine handles the cancellation operation by throwing a special cancelationexception
  • All kotlinx Pending functions (withcontext / delay, etc.) in coroutines are cancelable

    Canceling a scope cancels its children

    @Test
    fun test_cancel_2() = runBlocking<Unit> {
      val scope = CoroutineScope(Dispatchers.Default)
      val job1 = scope.launch {
          println("job1 started.")
          delay(10)
          println("job1 finished.")
      }
      val job2 = scope.launch {
          println("job2 started.")
          delay(100)
          println("job2 finished.")
      }
      delay(500)
    }

output:

job1 started.
job2 started.
job1 finished.
job2 finished.
Process finished with exit code 0

Note: runBlocking is the main coroutine. Start two child coroutines with a custom scope. Job1 and job2 are child coroutines in the scope. In line 14, the delay of 500 milliseconds is in the main process and is managed by the scope of the main process. Run the test code, and the program will exit after the suspend function delay 500 is completed. At this time, one of the subprocess job1 and job2 tasks started by scope takes 10 milliseconds and the other takes 100 milliseconds, and can also be completed. If the delay is 50 milliseconds, job1 can be completed during this period, but job2 cannot be completed.

Here's how to cancel the scope:

@Test
fun test_cancel_3() = runBlocking<Unit> {
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch {
        println("job1 started.")
        delay(10)
        println("job1 finished.")
    }
    val job2 = scope.launch {
        println("job2 started.")
        delay(100)
        println("job2 finished.")
    }
    scope.cancel()
    delay(500)
}

The result is

job1 started.
job2 started.

Process finished with exit code 0

In line 14, cancel the scope. You can see that job1 and job2 belong to the brother process in the scope. After the scope is cancelled, there will be no skin and hair. They are both cancelled. That is, canceling the scope cancels its child processes.

The cancelled subprocess does not affect other sibling subprocesses

@Test
fun test_cancel_4() = runBlocking<Unit> {
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch {
        println("job1 started.")
        delay(10)
        println("job1 finished.")
    }
    val job2 = scope.launch {
        println("job2 started.")
        delay(100)
        println("job2 finished.")
    }
    job1.cancel()
    delay(500)
}

result

job1 started.
job2 started.
job2 finished.

Process finished with exit code 0

Canceling job1 does not affect the execution of brother job2.

CancellationException

@Test
fun test_cancel_exception() = runBlocking<Unit> {
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch {
        try {
            println("job1 started.")
            delay(10)
            println("job1 finished.")
        } catch (e: Exception) {
            println(e.toString())
        }
    }
    job1.cancel("handle the cancellation exception!")
    job1.join()
}

Output

job1 started.
java.util.concurrent.CancellationException: handle the cancellation exception!

Process finished with exit code 0

There is also a cancelAndJoin():

@Test
fun test_cancel_exception() = runBlocking<Unit> {
    val scope = CoroutineScope(Dispatchers.Default)
    val job1 = scope.launch {
        try {
            println("job1 started.")
            delay(10)
            println("job1 finished.")
        } catch (e: Exception) {
            println(e.toString())
        }
    }
    job1.cancelAndJoin()
}

The effect is the same.
All kotlinx Pending functions (withcontext / delay, etc.) in coroutines are cancelable
For example, in the above example, the delay() suspend function can be cancel led when the wait is suspended.

Cancellation of CPU intensive tasks

  • isActive is an extended property that can be used in CoroutineScope to check whether a Job is active
  • ensureActive(), if the job is inactive, this method will immediately throw an exception.
  • The yield function will check the status of the associated process. If it has been cancelled, it will throw a cancelationexception in response. In addition, it will try to transfer the execution right of the thread to provide execution opportunities for other processes.

    @Test
    fun test_cpu_task() = runBlocking {
      val start = System.currentTimeMillis();
      val job = launch(Dispatchers.Default) {
          var i = 0
          var nextPrintTime = start
          while (i < 5) {
              if (System.currentTimeMillis() >= nextPrintTime) {
                  println("job is waiting ${i++}")
                  nextPrintTime += 1000
              }
          }
      }
      delay(1000)
      job.cancel()
      println("main ended.")
    }

    output:

    job is waiting 0
    job is waiting 1
    main ended.
    job is waiting 2
    job is waiting 3
    job is waiting 4
    
    Process finished with exit code 0

    Job is a CPU intensive task, which is printed every 1 second. Although the job was cancelled, the job finished executing while. To terminate a task, use isActive to add a judgment in the while condition:

    @Test
    fun test_cpu_task_active() = runBlocking {
      val start = System.currentTimeMillis();
      val job = launch(Dispatchers.Default) {
          var i = 0
          var nextPrintTime = start
          while (i < 5 && isActive) {
              if (System.currentTimeMillis() >= nextPrintTime) {
                  println("job is waiting ${i++}")
                  nextPrintTime += 1000
              }
          }
      }
      delay(1000)
      job.cancel()
      println("main ended.")
    }

    output:

    job is waiting 0
    job is waiting 1
    main ended.
    
    Process finished with exit code 0

    This isActive can be used to check the status of the process, but cannot throw an exception. That is, adding try catch does not catch any exceptions. If you want to achieve this effect, you can use the ensureActive() function to check isActive internally and throw an exception:

    @Test
    fun test_cpu_task_active_cancel() = runBlocking {
      val start = System.currentTimeMillis();
      val job = launch(Dispatchers.Default) {
          try {
              var i = 0
              var nextPrintTime = start
              while (i < 5) {
                  ensureActive()
                  if (System.currentTimeMillis() >= nextPrintTime) {
                      println("job is waiting ${i++}")
                      nextPrintTime += 1000
                  }
              }
          } catch (e: Exception) {
              println(e.toString())
          }
      }
      delay(1000)
      job.cancel()
      println("main ended.")
    }

    output:

    job is waiting 0
    job is waiting 1
    main ended.
    kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@720b7121
    
    Process finished with exit code 0

    You can see that the exception jobcancelationexception is thrown and caught.
    yeild can also throw exceptions and respond. It can also give thread execution rights to provide execution opportunities for other collaborations.

    @Test
    fun test_cpu_task_yield() = runBlocking {
      val start = System.currentTimeMillis();
      val job = launch(Dispatchers.Default) {
          try {
              var i = 0
              var nextPrintTime = start
              while (i < 5) {
                  yield()
                  if (System.currentTimeMillis() >= nextPrintTime) {
                      println("job is waiting ${i++}")
                      nextPrintTime += 1000
                  }
              }
          } catch (e: Exception) {
              println(e.toString())
          }
      }
      delay(1000)
      job.cancel()
      println("main ended.")
    }

    output:

    job is waiting 0
    job is waiting 1
    main ended.
    kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@478cdf05
    
    Process finished with exit code 0

    Side effects of collaborative process cancellation

  • Release resources in finally
  • use function: this function can only be used by objects that implement Coloseable. The close method will be called automatically at the end of the program, which is suitable for file objects.

    @Test
    fun test_release() = runBlocking {
      val job = launch {
          try {
              repeat(1000) { i ->
                  println("job is waiting $i ...")
                  delay(1000)
              }
          } catch (e: Exception) {
              println(e.toString())
          } finally {
              println("job finally.")
          }
      }
      delay(1500)
      println("job cancel.")
      job.cancelAndJoin()
      println("main ended.")
    }

    output:

    job is waiting 0 ...
    job is waiting 1 ...
    job cancel.
    kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@4f6ee6e4
    job finally.
    main ended.
    
    Process finished with exit code 0

    You can release resources in finally.
    Using the use function, you can omit the manual release of resources. When you don't use use use, you have to deal with it yourself:

    @Test
    fun test_not_use() = runBlocking {
      val filePath = ""
      val br = BufferedReader(FileReader(filePath))
      with(br) {
          var line: String?
          try {
              while (true) {
                  line = readLine() ?: break
                  println(line)
              }
          } catch (e: Exception) {
              println(e.toString())
          } finally {
              close()
          }
      }
    }

    use without additional processing:

    @Test
    fun test_use() = runBlocking {
      val path = ""
      BufferedReader(FileReader(path)).use {
          var line: String?
          while (true) {
              line = readLine()?:break
              println(line)
          }
      }
    }

    NonCancellable

    NonCancellable can be used to handle operations that cannot be cancelled.

Look at this example

@Test
fun test_cancel_normal() = runBlocking {
    val job = launch {
        try {
            repeat(100) { i ->
                println("job is waiting $i...")
                delay(1000)
            }
        } finally {
            println("job finally...")
            delay(1000)
            println("job delayed 1 second as non cancellable...")
        }
    }
    delay(1000)
    job.cancelAndJoin()
    println("main end...")
}

output

job is waiting 0...
job finally...
main end...

Process finished with exit code 0

If there are operations that cannot be cancelled in line 11 and line 12 in finally, this will not work. You can use NonCancellable:

@Test
fun test_non_cancellable() = runBlocking {
    val job = launch {
        try {
            repeat(100) { i ->
                println("job is waiting $i...")
                delay(1000)
            }
        } finally {
            withContext(NonCancellable) {
                println("job finally...")
                delay(1000)
                println("job delayed 1 second as non cancellable...")
            }
        }
    }
    delay(1000)
    job.cancelAndJoin()
    println("main end...")
}

output

job is waiting 0...
job finally...
job delayed 1 second as non cancellable...
main end...
Process finished with exit code 0

Timeout task

  • In many cases, the reason to cancel a collaboration is that it may timeout
  • withTimeoutOrNull performs a timeout operation by returning null instead of throwing an exception

    @Test
    fun test_timeout() = runBlocking {
      val job = withTimeout(1300) {
          repeat(5) {
              println("start job $it ...")
              delay(1000)
              println("end job $it ...")
          }
      }
    }

    output:

    start job 0 ...
    end job 0 ...
    start job 1 ...
    kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

    withTimeout, when more than 1300 milliseconds, the task will terminate, and then a TimeoutCancellationException will be thrown

    @Test
    fun test_timeout_return_null() = runBlocking {
      val res = withTimeoutOrNull(1300) {
          repeat(5) {
              println("start job $it ...")
              delay(1000)
              println("end job $it ...")
          }
          "Done"
      }
      println("Result: $res")
    }

    output:

    start job 0 ...
    end job 0 ...
    start job 1 ...
    Result: null
    
    Process finished with exit code 0

    withTimeoutOrNull will return a result in the last line of the code block. After normal completion, it will return the modified result. If timeout, it will return null. Example:

    @Test
    fun test_timeout_return_null() = runBlocking {
      val res = withTimeoutOrNull(1300) {
          try {
              repeat(5) {
                  println("start job $it ...")
                  delay(1000)
                  println("end job $it ...")
              }
              "Done"
          } catch (e: Exception) {
              println(e.toString())
          }
      }
      println("Result: $res")
    }

    output:

    start job 0 ...
    end job 0 ...
    start job 1 ...
    kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
    Result: null
    
    Process finished with exit code 0

    Exception handling of collaborative process

  • Context of collaboration

    • Combining elements in context
    • Inheritance of coroutine context
  • Exception handling of collaborative process

    • Propagation characteristics of anomalies
    • Exception capture
    • Global exception handling
    • Cancellation and exception
    • Abnormal aggregation

    What is the context of the collaboration

    CoroutineContext is a set of elements used to define the behavior of a coroutine. It consists of the following items:

  • Job: control the life cycle of the collaboration
  • CoroutineDispatcher: distribute tasks to appropriate threads
  • CoroutineName: the name of the collaboration process, which is very useful during debugging
  • CoroutineExceptionHandler: handle uncapped exceptions

    Combining elements in context

    Sometimes multiple elements need to be defined in the context of a collaboration, which can be implemented using the + operator. For example, you can specify a scheduler to start the process, and explicitly specify a name at the same time:

    @Test
    fun test_coroutine_context() = runBlocking<Unit> {
      launch(Dispatchers.Default + CoroutineName("test")) {
          println("running on thread: ${Thread.currentThread().name}")
      }
    }

    output:

    running on thread: DefaultDispatcher-worker-1 @test#2
    Process finished with exit code 0

    Inheritance of coroutine context

    For the newly created collaboration, its CoroutineContext will contain a new Job instance, which will help us control the lifecycle of the collaboration. The remaining elements will inherit from the parent class of CoroutineContext, which may be another collaboration or the CoroutineScope that created the collaboration.

    @Test
    fun test_coroutine_context_extend() = runBlocking<Unit> {
      // Create a collaboration scope
      val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("test"))
      println("${coroutineContext[Job]} , ${Thread.currentThread().name}")
      // Start a collaboration through the collaboration scope
      val job = scope.launch {
          println("${coroutineContext[Job]} , ${Thread.currentThread().name}")
          val result = async {
              println("${coroutineContext[Job]} , ${Thread.currentThread().name}")
              "Ok"
          }.await()
          println("result: $result")
      }
      job.join()
    }

    output:

    "coroutine#1":BlockingCoroutine{Active}@2ddc8ecb , main @coroutine#1
    "test#2":StandaloneCoroutine{Active}@5dd0b98f , DefaultDispatcher-worker-1 @test#2
    "test#3":DeferredCoroutine{Active}@4398fdb7 , DefaultDispatcher-worker-3 @test#3
    result: Ok
    
    Process finished with exit code 0

    runBlocking is the collaboration running in the main thread, job is the collaboration in the scope, and result is the collaboration in the job; Each layer of CoroutineContext will have a new job instance, BlockingCoroutine{Active}@2ddc8ecb, StandaloneCoroutine{Active}@5dd0b98f, DeferredCoroutine{Active}@4398fdb7; The remaining elements inherit from the parent class of CoroutineContext, such as the job under the scope, and inherit dispatchers from the parent scope IO and CoroutineName("test")

Context of coroutine = default value + inherited CoroutineContext + parameter

  • Some elements contain default values: dispatchers Default is the default coroutine dispatcher, and "coroutine" is the default coroutine name;
  • The inherited CoroutineContext is the CoroutineContext of CoroutineScope or its parent process;
  • The parameters passed into the collaboration builder take precedence over the inherited context parameters, so the corresponding parameter values are overwritten.

    @Test
    fun test_coroutine_context_extend2() = runBlocking<Unit> {
      val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
          println("handle exception: $exception")
      }
      val scope = CoroutineScope(Job() + Dispatchers.Main + CoroutineName("test") + coroutineExceptionHandler)
      val job = scope.launch(Dispatchers.IO) {
          println("${coroutineContext[Job]} , ${Thread.currentThread().name}")
          3/0
          println("end of job...")
      }
      job.join()
    }

    output:

    "test#2":StandaloneCoroutine{Active}@39e6aad4 , DefaultDispatcher-worker-2 @test#2
    handle exception: java.lang.ArithmeticException: / by zero
    
    Process finished with exit code 0

    As can be seen from the above example, the constructor passes in the parameter dispatchers IO covers the original Main; The name comes from CoroutineName("test") in scope; The exception caused by the divide by 0 operation in the collaboration task is caught by the user-defined exceptionHandler.

    Necessity of exception handling

    When there are some unexpected situations in the application, it is very important to provide the user with an appropriate experience. On the one hand, the application crash is a very bad experience. In addition, the user must be able to give correct prompt information when the operation fails.

    Abnormal propagation

    There are two forms of Builders:

  • Auto propagation exception (launch and actor)
  • Expose exceptions to users (async and produce)
    When these constructors are used to create a root coprocessor (the coprocessor is not a child of another coprocessor), the exception of the former type of constructor will be thrown at the first time it occurs; The latter relies on users to end up consuming exceptions, such as via await or receive Take an example:

    @Test
    fun test_exception() = runBlocking<Unit> {
      val job = GlobalScope.launch {
          try {
              throw IllegalArgumentException("exception from job1")
          } catch (e: Exception) {
              println("Caught exception: $e")
          }
      }
      job.join()
    
      val job2 = GlobalScope.async {
          1 / 0
      }
      try {
          job2.await()
      } catch (e: Exception) {
          println("Caught exception: $e")
      }
    }

    launch is an automatic propagation exception, which is thrown the first time it occurs; async is used to expose exceptions to the user and rely on the user to consume exceptions. It is called through the await function. The output is as follows

    Caught exception: java.lang.IllegalArgumentException: exception from job1
    Caught exception: java.lang.ArithmeticException: / by zero
    
    Process finished with exit code 0

    For async started coroutines, if the user does not handle them, no exceptions will be exposed. as follows

      @Test
      fun test_exception() = runBlocking<Unit> {
          val job = GlobalScope.launch {
              try {
                  throw IllegalArgumentException("exception from job1")
              } catch (e: Exception) {
                  println("Caught exception: $e")
              }
          }
          job.join()
    
          val job2 = GlobalScope.async {
              println("job2 begin")
              1 / 0
          }
          delay(1000)
    //        try {
    //            job2.await()
    //        } catch (e: Exception) {
    //            println("Caught exception: $e")
    //        }
      }

    output

    Caught exception: java.lang.IllegalArgumentException: exception from job1
    job2 begin
    Process finished with exit code 0

    job2 executes, but does not throw an exception. User consumption exception is required.

    Anomaly of non root coprocess

    In the processes created by other processes, the exceptions generated will always be propagated.

    @Test
    fun text_exception_2() = runBlocking<Unit> {
      val scope = CoroutineScope(Job())
      val job = scope.launch {
          async {
              println("async started a coroutine...")
              1/0
          }
      }
      delay(100)
    }

    output:

    async started a coroutine...
    Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.ArithmeticException: / by zero

Characteristics of anomaly propagation

When a coroutine fails due to an exception, it propagates the exception and passes it to its parent. Next, the parent will perform the following steps:

  • Cancel its own children
  • Cancel itself
  • Propagate the exception and pass it to its parent

    SupervisorJob

  • When using SupervisorJob, the running failure of one sub process will not affect other sub processes. The SupervisorJob does not propagate exceptions to its parent. It lets the child process handle exceptions by itself.
  • This requirement is common in defining the UI component of a job in the scope. If the execution of any UI sub job fails, it is not always necessary to cancel the whole UI component. However, if the UI component is destroyed and its results are no longer needed, it is necessary to fail the execution of all sub jobs.

    @Test
    fun text_supervisor_job1() = runBlocking<Unit> {
      val scope = CoroutineScope(SupervisorJob())
      val job1 = scope.launch {
          println("child 1 start.")
          delay(1000)
          1 / 0
      }
      val job2 = scope.launch {
          println("child 2 start.")
          delay(5000)
          println("child 2 end.")
      }
      joinAll(job1, job2)
    }

    output

    child 1 start.
    child 2 start.
    Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.ArithmeticException: / by zero
    ...
    child 2 end.
    Process finished with exit code 0

    In the above example, the supervisor job is used to start two sub processes job1 and job2. Both jobs are executed. There is a division by zero operation in job1 and an exception is thrown, but job2 is not affected. However, if val scope = CoroutineScope(Job()), both sub processes will terminate

    @Test
    fun text_supervisor_job2() = runBlocking<Unit> {
      val scope = CoroutineScope(Job())
      val job1 = scope.launch {
          println("child 1 start.")
          delay(1000)
          1 / 0
      }
      val job2 = scope.launch {
          println("child 2 start.")
          delay(5000)
          println("child 2 end.")
      }
      joinAll(job1, job2)
    }

    result

    child 1 start.
    child 2 start.
    Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.ArithmeticException: / by zero
    ...
    Process finished with exit code 0

    supervisorScope

    When the job itself fails to execute, all sub jobs will be cancelled.

    @Test
    @Test
    fun text_supervisor_scope() = runBlocking<Unit> {
      try {
          supervisorScope {
              val job1 = launch {
                  println("child 1 start.")
                  delay(50)
                  println("child 1 end.")
              }
              val job2 = launch {
                  println("child 2 start.")
                  delay(5000)
                  println("child 2 end.")
              }
              println("in supervisor scope...")
              delay(1000)
              1 / 0
          }
      } catch (e: Exception) {
          println("caught exception: ${e.toString()}")
      }
    }

    output

    in supervisor scope...
    child 1 start.
    child 2 start.
    child 1 end.
    caught exception: java.lang.ArithmeticException: / by zero
    Process finished with exit code 0

    Directly use the supervisorScope to start the collaboration. The two subprocesses job1 and job2 are printed, delayed and divided by zero. You can see that the division by zero operation throws an exception. During this period, job1 delay time is short and the execution is completed, but job2 is not completed and is cancelled.

    Exception capture

  • Use CoroutineExceptionHandler to catch the exception of the coroutine
  • Exceptions are caught when the following conditions are met:

    • Timing: the exception is thrown by the coroutine that automatically throws the exception (when using launch instead of async)
    • Location: in the CoroutineContext of CoroutineScope or in a root process (the direct child process of CoroutineScope or supervisorScope).
    @Test
    fun test_exception_handler() = runBlocking<Unit> {
      val handler = CoroutineExceptionHandler { _, exception ->
          println("caught exception: $exception")
      }
      val job = GlobalScope.launch(handler) {
          throw IllegalArgumentException("from job")
      }
      val deferred = GlobalScope.async(handler) {
          throw ArithmeticException("from deferred")
      }
      job.join()
      deferred.await()
    }

    output

    caught exception: java.lang.IllegalArgumentException: from job
    java.lang.ArithmeticException: from deferred
    ...
    Process finished with exit code 255

    It can be seen that the handler captures the launch started coroutine (auto throw), but does not capture the async started coroutine (non auto throw); And GlobalScope is a root process.

    @Test
    fun test_exception_handler2() = runBlocking<Unit> {
      val handler = CoroutineExceptionHandler { _, exception ->
          println("caught exception: $exception")
      }
      val scope = CoroutineScope(Job())
      val job1 = scope.launch(handler) {
          launch {
              throw IllegalArgumentException("from job1")
          }
      }
      
      job1.join()
    }

    output:

    caught exception: java.lang.IllegalArgumentException: from job1
    Process finished with exit code 0

    In the above example, the handler is installed on the external process and can be captured. However, if it is installed on an internal process, it cannot be captured, as shown below

    @Test
    fun test_exception_handler4() = runBlocking<Unit> {
      val handler = CoroutineExceptionHandler { _, exception ->
          println("caught exception: $exception")
      }
      val scope = CoroutineScope(Job())
      val job1 = scope.launch {
          launch(handler) {
              throw IllegalArgumentException("from job1")
          }
      }
    
      job1.join()
    }

    output:

    Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.IllegalArgumentException: from job1
    ...
    Process finished with exit code 0

    Global exception handling in Android

  • Global exception handling can obtain all unhandled exceptions that are not handled by the collaboration, but it can not capture exceptions. Although it can not prevent program crash, the global exception handler is still very useful in scenarios such as program debugging and exception reporting.
  • You need to create the META-INF/services directory in the classpath and create a directory named kotlinx coroutines. Coroutineexceptionhandler file. The file content is the full class name of our global exception handler.
    Take an example of an exception caught in android:

    override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
      val handler = CoroutineExceptionHandler { _, e ->
          Log.d(TAG, "onCreate: caught exception: $e")
      }
      
      binding.btnFetch.setOnClickListener {
          lifecycleScope.launch(handler) {
              Log.d(TAG, "onCreate: on click")
              "abc".substring(10)
          }
      }
    }

    If the handler is not used, the application will crash; With handler, you can catch and handle exceptions to avoid application crash.
    But what if it's not captured? There is another way to get the global exception information, which is the above-mentioned Android global exception handling.
    As mentioned above, create the resource/META-INF/services / directory src/main in the project directory and create kotlinx coroutines. Coroutineexceptionhandler file.

    The file content is the full class name of the global exception handler. as follows

    The global exception handling classes are as follows. When implementing the handleException method, implement logic such as printing and log collection according to your business needs.

    class GlobalCoroutineExceptionHandler: CoroutineExceptionHandler {
      override val key = CoroutineExceptionHandler
    
      override fun handleException(context: CoroutineContext, exception: Throwable) {
          Log.d("GlobalException", "Unhandled exception: $exception")
      }
    }

    In this way, exceptions that are not caught will be obtained in the modified method.

    Cancellation and exception

  • Cancelation is closely related to exceptions. Cancelationexception is used to cancel in the collaboration process, and the exception will be ignored.
  • When a child collaboration is cancelled, its parent collaboration is not cancelled.
  • If a coroutine encounters an exception other than cancelationexception, it will use the exception to cancel its parent coroutine. When all the child processes of the parent process are completed, the exception will be handled by the parent process.

    @Test
    fun test_cancel_and_exception1() = runBlocking<Unit> {
      val job = launch {
          println("parent started.")
          val child = launch {
              println("child started.")
              delay(10000)
              println("child ended.")
          }
          yield()
          child.cancelAndJoin()
          yield()
          println("parent is not cancelled.")
      }
    }

    output

    parent started.
    child started.
    parent is not cancelled.
    Process finished with exit code 0

    The program runs normally, the child ended does not print, and the child is cancelled.
    There will be a jobcancelationexception. You can use try catch to capture. As follows:

    @Test
    fun test_cancel_and_exception2() = runBlocking<Unit> {
      val job = launch {
          println("parent started.")
          val child = launch {
              try {
                  println("child started.")
                  delay(10000)
                  println("child ended.")
              } catch (e: Exception) {
                  println("child exception: $e")
              } finally {
                  println("child finally.")
              }
          }
          yield()
          child.cancelAndJoin()
          yield()
          println("parent is not cancelled.")
      }
    }

    output

    parent started.
    child started.
    child exception: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#3":StandaloneCoroutine{Cancelling}@294425a7
    child finally.
    parent is not cancelled.
    Process finished with exit code 0
    
    @Test
    fun test_cancel_and_exception3() = runBlocking<Unit> {
      val handler = CoroutineExceptionHandler { _, e ->
          println("handle the exception: $e")
      }
      val parent = GlobalScope.launch(handler) {
          val child1 = launch {
              try {
                  println("child1 started.")
                  delay(Long.MAX_VALUE)
                  println("child1 ended.")
              } finally {
                  withContext(NonCancellable) {
                      println("child1 cancelled, but exception is not handled until all children are terminated")
                      delay(100)
                      println("child1 finished in the non cancellable block")
                  }
              }
          }
          val child2 = launch {
              println("child2 started.")
              "abc".substring(10)
              delay(100)
              println("child2 ended.")
          }
      }
      parent.join()
    }

    output

    child1 started.
    child2 started.
    child1 cancelled, but exception is not handled until all children are terminated
    child1 finished in the non cancellable block
    handle the exception: java.lang.StringIndexOutOfBoundsException: String index out of range: -7
    Process finished with exit code 0

    When all the child processes of the parent process are completed, the exception will be handled by the parent process.

    Abnormal aggregation

    When multiple sub processes of a process fail due to an exception, the first exception is generally taken for processing. All other exceptions that occur after the first exception are bound to the first exception.

    @Test
    fun exception_aggregation() = runBlocking<Unit> {
      val handler = CoroutineExceptionHandler { _, e ->
          println("handle the exception: $e")
          println("the other exceptions: ${e.suppressedExceptions}")
      }
      val job = GlobalScope.launch(handler) {
          launch {
              try {
                  delay(Long.MAX_VALUE)
              } finally {
                  throw ArithmeticException("first exception")
              }
          }
          launch {
              try {
                  delay(Long.MAX_VALUE)
              } finally {
                  throw ArithmeticException("second exception")
              }
          }
          launch {
              "abc".substring(10)
          }
      }
      job.join()
    }

    output:

    handle the exception: java.lang.StringIndexOutOfBoundsException: String index out of range: -7
    the other exceptions: [java.lang.ArithmeticException: second exception, java.lang.ArithmeticException: first exception]
    Process finished with exit code 0

    e.suppressedExceptions can be used in the exception handler to get other exceptions.

    Study notes, tutorials from:
    https://www.bilibili.com/vide...
    Thank you~

Keywords: Android kotlin coroutine

Added by AudiS2 on Tue, 04 Jan 2022 12:37:27 +0200