Public number: byte array, keen to share Android system source code parsing, Jetpack source code parsing, popular open source library source code parsing and other essential knowledge interview

Recently, I have been learning about kotlin coroutines, and the best learning materials are naturally the official learning documents. After reading them, I have the idea of translating the official documents. Before and after spent close to a month time, a total of nine articles, here also share out, hope to help readers. Limited by personal knowledge, some translation is not too smooth, also hope that readers can put forward their opinions

Coroutines official documentation: Coroutines – Guide

Coroutines-cn-guide coroutines-cn-guide

Coroutine official document Chinese translator: leavesC

[TOC]

This section discusses coroutines about handling and canceling exceptions. We already know that canceling the coroutine causes a CancellationException to be thrown at the start of the suspension, which the coroutine mechanism ignores. But what happens if an exception is thrown during cancellation, or if multiple subcoroutines of a coroutine throw an exception?

(2) Exception Propagation

Coroutine builders come in two types: they propagate exceptions automatically (launch and Actor) and expose exceptions to the user (Async and Product). The former will anomalies as uncaught exception, similar to the Java Thread. UncaughtExceptionHandler, while the latter need by the developer to handle the exception, For example, by await or receive (Product and receive are described in the Channels section)

This can be demonstrated by a simple example of creating coroutines in GlobalScope:

import kotlinx.coroutines.*

fun main(a) = runBlocking {
    val job = GlobalScope.launch {
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async {
        println("Throwing exception from async")
        throw ArithmeticException() // Nothing is printed, relying on user to call await
    }
    try {
        deferred.await()
        println("Unreached")}catch (e: ArithmeticException) {
        println("Caught ArithmeticException")}}Copy the code

Running result:

Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException
Copy the code

Second, coroutines exception handler (CoroutineExceptionHandler)

If you don’t want to put all the exceptions are printed to the console, CoroutineExceptionHandler context elements can be a catch block, coroutines global general here for custom logging or exception handling. It is similar to the use of Thread Thread. UncaughtExceptionHandler

On the JVM, can be registered ServiceLoader CoroutineExceptionHandler to redefine all coroutines exception handler. Exception handler is similar to the Thread. DefaultUncaughtExceptionHandler, which registered in no other specific handler. On Android, uncaughtExceptionPreHandler as global coroutines exception handler

CoroutineExceptionHandler only on exception is not expected to handle by the user calls, so registered in async such coroutines constructor it doesn’t have any effect

import kotlinx.coroutines.*

fun main(a) = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception")}val job = GlobalScope.launch(handler) {
        throw AssertionError()
    }
    val deferred = GlobalScope.async(handler) {
        throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
    }
    joinAll(job, deferred)
//sampleEnd    
}
Copy the code

Running result:

Caught java.lang.AssertionError
Copy the code

A) Cancellation and exceptions. B) Cancellation and exceptions.

Cancellations and exceptions go hand in hand. Coroutines use cancellationExceptions internally to cancel, and all handlers ignore such exceptions, so they serve only as an additional source of debugging information that can be caught with a catch block. When you take a coroutine with job. cancel, the coroutine stops running, but its parent coroutine is not cancelled

import kotlinx.coroutines.*

fun main(a) = runBlocking {
//sampleStart
    val job = launch {
        val child = launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                println("Child is cancelled")
            }
        }
        yield()
        println("Cancelling child")
        child.cancel()
        child.join()
        yield()
        println("Parent is not cancelled")
    }
    job.join()
//sampleEnd    
}
Copy the code

Running result:

Cancelling child
Child is cancelled
Parent is not cancelled
Copy the code

If a coroutine encounters an exception other than CancellationException, it uses that exception to cancel its parent. Unable to override this behavior, it does not depend on CoroutineExceptionHandler for so as to realize the concurrent structured, provide stability for coroutines hierarchy. When all of the parent’s children terminate, the parent handles the original exception

This is why in these examples, will always CoroutineExceptionHandler passed as a parameter to created in GlobalScope coroutines in reason. Set CoroutineExceptionHandler to within the scope of the main runBlocking start coroutines is meaningless, because although set up exception handler, coroutines in its level exceptions will be cancelled after the throw

import kotlinx.coroutines.*

fun main(a) = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception")}val job = GlobalScope.launch(handler) {
        launch { // the first child
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(100)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
//sampleEnd    
}
Copy the code

Running result:

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
Caught java.lang.ArithmeticException
Copy the code

CoroutineExceptionHandler will wait until all child coroutines running after the callback. After the second child throws an exception will lead to end of the first child linkage operation, then the exception to CoroutineExceptionHandler processing

4. Exception Aggregation

What happens if multiple subcoroutines of a coroutine throw an exception? The general rule is “the first exception wins,” so the first exception thrown will be passed to the exception handler for processing, but this may cause the exception to be lost. For example, if in a collaborators process after an exception is thrown, the collaborators in the finally block cheng throws an exception, at this point the second collaborators cheng exception will not be passed to the CoroutineExceptionHandler

One solution is to throw each exception separately. Await should have the same mechanism to avoid behavior inconsistencies that result in implementation details of the coroutine (whether or not it delegates part of its work to its children) being leaked to its exception handler

import kotlinx.coroutines.*
import java.io.*

fun main(a) = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception with suppressed ${exception.suppressed.contentToString()}")}val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                throw ArithmeticException()
            }
        }
        launch {
            delay(100)
            throw IOException()
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}
Copy the code

Note: this code only works on JDK7+ versions that support suppressed exceptions

Running result:

Caught java.io.IOException with suppressed [java.lang.ArithmeticException]
Copy the code

Exceptions that cause a coroutine to stop are transparently passed by default and are not wrapped

import kotlinx.coroutines.*
import java.io.*

fun main(a) = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught original $exception")}val job = GlobalScope.launch(handler) {
        val inner = launch {
            launch {
                launch {
                    throw IOException()
                }
            }
        }
        try {
            inner.join()
        } catch (e: CancellationException) {
            println("Rethrowing CancellationException with original cause")
            throw e
        }
    }
    job.join()
//sampleEnd    
}
Copy the code

Running result:

Rethrowing CancellationException with original cause
Caught original java.io.IOException
Copy the code

Even capture the inner exception information has been canceled, but eventually passed to CoroutineExceptionHandler or internal real inner exception information

5. Supervision

As we have studied previously, cancellation is a two-way relationship that propagates throughout the coroutine hierarchy. But what if you need one-way cancellation?

A good example of such a requirement defines the UI components of a Job in some scope. If any of the UI component’s subtasks fail, it is not necessarily necessary to cancel (indeed terminate) the entire UI component at this point. But if the lifecycle of a UI component ends (and its Job is canceled), then all subjobs must be canceled because their results are no longer required

Another example is a server process that generates several subjobs and needs to monitor their execution, track when they fail, and restart only those subjobs that fail

5.1 Supervision Job

For these purposes, you can use supervisorJobs. It is similar to a regular Job, with the exception that cancellations are propagated downward only. This is easy to demonstrate with an example:

import kotlinx.coroutines.*

fun main(a) = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // launch the first child -- its exception is ignored for this example (don't do this in practice!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
            println("First child is failing")
            throw AssertionError("First child is cancelled")}// launch the second child
        val secondChild = launch {
            firstChild.join()
            // Cancellation of the first child is not propagated to the second child
            println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // But cancellation of the supervisor is propagated
                println("Second child is cancelled because supervisor is cancelled")}}// wait until the first child fails & completes
        firstChild.join()
        println("Cancelling supervisor")
        supervisor.cancel()
        secondChild.join()
    }
}
Copy the code

Running result:

First child is failing
First child is cancelled: true, but second one is still active
Cancelling supervisor
Second child is cancelled because supervisor is cancelled
Copy the code

5.2 Supervision Scope

For scope concurrency, you can use supervisorScope instead of coroutineScope for the same purpose. It propagates cancellation operations in only one direction, and cancels all children only if it fails itself. Like coroutineScope, it also waits for all child elements to finish running before ending

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main(a) = runBlocking {
    try {
        supervisorScope {
            val child = launch {
                try {
                    println("Child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("Child is cancelled")}}// Give our child a chance to execute and print using yield 
            yield()
            println("Throwing exception from scope")
            throw AssertionError()
        }
    } catch(e: AssertionError) {
        println("Caught assertion error")}}Copy the code

Output results:

Child is sleeping
Throwing exception from scope
Child is cancelled
Caught assertion error
Copy the code

The following example shows how unidirectional propagation of cancellations is in the supervisorScope. Exceptions to subcoroutines do not cause other subcoroutines to cancel

fun main(a) = runBlocking {
    supervisorScope {
        val child1 = launch {
            try {
                for (time in 1..Long.MAX_VALUE) {
                    println("Child 1 is printing: $time")
                    delay(1000)}}finally {
                println("Child 1 is cancelled")}}val child2 = launch {
            println("Child 2 is sleeping")
            delay(3000)
            println("Child 2 throws an exception")
            throw AssertionError()
        }
    }
}
Copy the code

Running result:

Child 1 is printing: 1
Child 2 is sleeping
Child 1 is printing: 2
Child 1 is printing: 3
Child 1 is printing: 4
Child 2 throws an exception
Exception in thread "main" java.lang.AssertionError
Child 1 is printing: 5
Child 1 is printing: 6· · · · · ·Copy the code

5.3 Exceptions in supervised Coroutines

Another important difference between a regular job and a supervisor job is exception handling. Each child should handle its own exceptions through an exception handling mechanism. This is due to the fact that the supervisorScope’s neutron element failures are not transmitted to the parent

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main(a) = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception") 
    }
    supervisorScope {
        val child = launch(handler) {
            println("Child throws an exception")
            throw AssertionError()
        }
        println("Scope is completing")
    }
    println("Scope is completed")}Copy the code

Running result:

Scope is completing
Child throws an exception
Caught java.lang.AssertionError
Scope is completed
Copy the code