Coroutine 学习(一)挂起,CoroutineScope,异常
房顶上的铝皮水塔
2021年09月27日 21:07
收录于文集
共19篇

理解挂起

在我个人的理解中,挂起、挂起函数指的是当前的函数可以被「搁置」,不影响主流程的运行

代码块
JavaScript
自动换行
复制代码
val job = GlobalScope.launch {
        delay(5000)
        println("the job is done")
    }
//    job.join()
    println("hello world")
复制成功

在以上的代码中,launch中的函数会立刻执行,但是需要等待5s。由于挂起函数有不影响主流程的特性,所以会先输出hello world,等到5s后再打印the job is done。

代码块
JavaScript
自动换行
复制代码
val job = GlobalScope.launch {
        delay(5000)
        println("the job is done")
    }
    job.join()
    println("hello world")
复制成功

但是在这里的代码中,由于join函数会先挂起当前的协程,等到job执行完成。当前的协程是什么? 当然是由runBlocking引导的协程,所以会先打印the job is done.

CoroutineContext

协程总是运行在一些以CoroutineContext类型为代表的上下文中。协程上下文在类型上更接近一个List。CoroutineContext包含一个调度器

CoroutineScope

CoroutineScope会跟踪它使用的launch和aysnc创建的所有协程。ViewModel有viewModelScope,LifeCycle有lifecycleScope。每一个协程构建器都是CoroutineScope的扩展函数,并且继承了其中的CoroutineContext。所以当使用CoroutineContext的协程构建器创建新的协程时,会继承CoroutineContext中的信息

代码块
JavaScript
自动换行
复制代码
class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}
复制成功

Job表示协程的句柄,可以通过这个类的实例来管理协程的生命周期

协程的异常处理

launch和async的对于异常的处理机制不同。当使用这两种构建器创建根协程时,launch会将异常视作未捕获异常,async需要对异常手动处理:

代码块
JavaScript
自动换行
复制代码
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = GlobalScope.launch { // launch 根协程
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async { // async 根协程
        println("Throwing exception from async")
        throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}
复制成功

这里的打印行为是通过CoroutineExceptionHandler处理的。可以自定定义CoroutineExceptionHandler:

代码块
JavaScript
自动换行
复制代码
val exceptionHandler = CoroutineExceptionHandler {_, exception ->
        println(exception.stackTrace)
}

val job2 = GlobalScope.launch(job + exceptionHandler){

}
复制成功

取消与异常:

在下面的代码中,父协程创建的实例是job,子协程创建的实例是child。使用的是Global.launch中调度的子协程,所以父子协程都具有相同的CoroutineContext。yield表示让出当前的协程调度器,给使用者同一个协程调度器的另外一个协程使用,这个协程就是child。但是很明显,这个让给子协程的动作好像只做了一下,如果是Dispatcher一直在子协程手中,则父协程不能有机会打印后面的内容。

代码块
JavaScript
自动换行
复制代码
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()
复制成功

job取消主动会抛出异常CancellationException,这个异常会被异常handler处理,但是不会结束父协程,所以打印的结果是:

子协程的取消不会影响父协程的取消,但是如果子协程遇到了CancellationException之外的异常,异常会被ExceptionHandler处理,父协程也随之结束:

代码块
JavaScript
自动换行
复制代码
  val r = GlobalScope.launch(exceptionHandler) {
        val child = launch {
            println(" child throw exception")
            throw java.lang.ArithmeticException()
        }
        delay(Long.MAX_VALUE)
    }
    r.join()
复制成功

输出:

代码块
JavaScript
自动换行
复制代码
 child throw exception
exception: [Ljava.lang.StackTraceElement;@7db621c2
复制成功

下面的代码展示了多个子协程的情况,如果有其中一个子协程抛出异常,父协程也会结束,其他的子协程也被迫关闭:

代码块
JavaScript
自动换行
复制代码
 val root = GlobalScope.launch(exceptionHandler) {
        val child1 = launch {
            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")
                }
            }
        }

        val child2 = launch { // 第二个子协程
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
        
        delay(Long.MAX_VALUE)
    }
    root.join()
复制成功

多个子协程出现的异常只会捕捉第一个。

如果取消父协程,子协程也会被取消吗?

代码块
JavaScript
自动换行
复制代码
// 父协程取消 子协程会被取消吗?
    val root  = GlobalScope.launch(exceptionHandler) {
        val child = launch {
            try{
                delay(Long.MAX_VALUE)
                println("ending of child: normally")
            }finally {
                println("ending of child: by parent")
            }
        }
        delay(Long.MAX_VALUE)
    }
    println("cancel parent")
    root.cancel()
    delay(4000)
复制成功

代码块
JavaScript
自动换行
复制代码
cancel parent
ending of child: by parent
复制成功

所以总结一下父子协程的关系:

覆巢之下无完卵: 父协程取消 子协程也取消 而且是都要取消

子不孝 父之过: 子协程取消 父协程也要取消

所以是一种双向关系

SupervisorJob实现单向关系

firstChild被取消了,但是并不会让secondChild停止,同时父协程也没有被停止。

代码块
JavaScript
自动换行
复制代码
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // 启动第一个子作业——这个示例将会忽略它的异常(不要在实践中这么做!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("The first child is failing")
            throw AssertionError("The first child is cancelled")
        }
        // 启动第二个子作业
        val secondChild = launch {
            firstChild.join()
            // 取消了第一个子作业且没有传播给第二个子作业
            println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // 但是取消了监督的传播
                println("The second child is cancelled because the supervisor was cancelled")
            }
        }
        // 等待直到第一个子作业失败且执行完成
        firstChild.join()
        println("Cancelling the supervisor")
        supervisor.cancel()
        secondChild.join()
    }
复制成功

我们可以使用supervisorScope实现类似的事情:

  1. 类似于SupervisoJob的单项传播

  2. 向coroutineScope一样,等到所有子协程结束之后结束自身

代码块
JavaScript
自动换行
复制代码
try {
        supervisorScope {
            val child = launch {
                try {
                    println("The child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("The child is cancelled")
                }
            }
            // 使用 yield 来给我们的子作业一个机会来执行打印
            yield()
            println("Throwing an exception from the scope")
            throw AssertionError()
        }
    } catch(e: AssertionError) {
        println("Caught an assertion error")
    }
复制成功

输出:

代码块
JavaScript
自动换行
复制代码
The child is sleeping
Throwing an exception from the scope
The child is cancelled
Caught an assertion error
复制成功

总结:

  1. 协程构建器中都有一个默认的CoroutineContext (协程上下文)

  2. 协程上下文就像List一样,我们可以往里面添加Dispatcher Job CoroutineExceptionHandler 等等

  3. 所有的协程构建器都是CoroutineScope的扩展函数,而CoroutineContext则是它的一个属性。CoroutineScope指定协程构建器,同时将自身的CoroutineContext用来配置构建器的【上下文信息】(dispatcher exceptionhandler....)

  4. 子协程表示继承了父协程CoroutineScope的协程。普通的CoroutineScope(也是普通的CoroutineContext)中,异常传递是双向的。但是这种双向传递关系可以被打破,通过在CoroutineContext中设置SupervisorJob或者直接通过supervisorScope构建协程。