聊一聊Kotlin中的协程

873 阅读14分钟

1.关于异步的问题

你用过线程对吧?
遇到I/O,网络等问题你是怎么处理逻辑的?
回调? Rx?

对于异步问题我们一般使用CallBack,但是随着代码的迭代我们发现:

  1. 这样会将控制流分割成不同控制流,增加代码复杂性
  2. 如果嵌套层级过多,会造成回调地狱,同时控制流数量也可能呈指数上升

Future

Java 5 推出的接口,用来代表一个异步运算未来的值。
它帮我们封装了异步的细节,可以解决控制流分叉,和嵌套地狱的问题,但是在主线程调用会阻塞

fun testCoroutine() {
    val future = requestDataAsync()
    doSomethingElse()
    processData(future.get())
}

Reactive

Java8 CompletableFuture, 以及rx系列
Rx风格可以有效解决嵌套回调的问题,但是没有解决控制流分叉的问题,RxJava是一个优秀的库,拥有丰富的操作符,在处理复杂的事件流方面有着强大的能力,但是有着一定的学习成本,同时并不能保证同事也具备同样的能力

思考:有没有更简洁更符合人类思维的写法呢? 就如同写同步逻辑那样? 我们看下 Kotlin Coroutine

2.Coroutine

官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

协程就像非常轻量级的线程
线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,代价比较小,协程是由开发者控制的(可以主动挂起或者恢复执行)。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建多个协程。

任务之间的调度而是协作式的,非抢占式

suspend fun testCoroutine() {

    GlobalScope.launch(Main) {
    
        val dataDeferred = getDataAsync()
        
        doSomethingElse()
        val data = dataDeferred.await()
        processData(data)
    }

    Thread.sleep(1000)
    doSomething()
}

fun getDataAsync(): Deferred<Unit> {
    return GlobalScope.async { // 启动一个异步协程去执行耗时任务
        requestData() 
    }
}

3.抢夺式 vs 协作式

在CPU的一个内核中,同一时刻只有一个进程在执行,这么多进程,CPU的时间片该如何分配呢?

image

协作式多任务:

早期的操作系统采用的就是协作时多任务,即:
由进程主动让出执行权 eg:当前进程需等待IO操作,主动让出CPU,由系统调度下一个进程。隐患是:单个进程可以完全霸占CPU,由于计算机中的进程良莠不齐,如果健壮性比较差的进程,运行中途发生了死循环、死锁等,会导致整个系统陷入瘫痪!

抢占式多任务:

由操作系统决定执行权,因为操作系统具有从任何一个进程取走控制权和使另一个进程获得控制权的能力。系统公平合理地为每个进程分配时间片,进程用完就休眠,甚至时间片没用完,但有更紧急的事件要优先执行,也会强制让进程休眠。线程也做成了抢占式多任务,同时也带来了新的—线程安全问题

4.Coroutine vs Thread

进程,线程,协程关系如下图:

image

我们模拟下线程和协程执行逻辑流流程:

  • 多线程情形下的多逻辑能力严重依赖于程序申请到的执行流/线程的数量。
    线程相对比较重,不仅体积大,还要经常进出内核。在这种模式下,线程的阻塞等待浪费了大量内存。

image

  • 协程不会出现图1这种阻塞的情况,一但遇到阻塞,那么要么从池子里挑下一个准备好的协程跑,要么由阻塞的协程指定由谁接着它跑,没有CPU时间的浪费。

image

关于线程和协程的区别:

  1. 协程是编译器级的(用户态),而线程是操作系统级的(内核态)。
    协程通常是由编译器来实现的机制。线程看起来也在语言层次,但是内在原理却是操作系统先有这个东西,然后通过一定的API暴露给用户使用,两者在这里有不同。
  2. 用协程来做的东西,用线程或进程通常也是一样可以做的,但往往多了许多加锁和通信的操作。
  3. 线程是抢占式,而协程协作式,需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
  4. 协程并非取代线程,是抽象于线程之上,线程是被分割的CPU资源,,协程是组织好的代码流程,,协程需要线程来承载运行,协程通过Interceptor来间接使用线程这个资源, Interceptor可以关联任意线程或线程池。

在协程代码中使用锁之类的并发工具无异于画蛇添足,建议在编写协程代码时尽量避免对外部作用域的可变变量进行引用,协程间的通讯可以使用Channel。

5.Coroutine 优缺点

  1. 轻量:
    您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并发操作。

  2. 内存泄漏更少:
    使用结构化并发机制在一个作用域内执行多项操作。

  3. 内置取消支持:
    取消操作会自动在运行中的整个协程层次结构内传播。

  4. Jetpack 集成:
    许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

协程真的没有缺点吗?
首先由于协程运行在线程中,线程的安全问题对于协程也是存在的,只是协程在很大程度上避免了。
另外在高并发的情况下协程很可能会发生一些不可描述的问题。

6.Coroutine builders

有三种构建器帮助我们创建协程

1. launch:Job

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    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
}

launch 没有返回值,或者说返回只是 job ,能够知道任务的状态,却不能携带返回结果。
launch 常用来运行不需要操作结果的协程(如文件删除,创建等)
launch 是CoroutineScope的一个扩展函数
launch 方法有三个参数:1.协程下上文;2.协程启动模式;3.协程体:block是一个带接收者的函数字面量,接收者是CoroutineScope

Demo:
GlobalScope.launch(Main) {
    val dataDeferred = getDataAsync()
    doSomethingElse()
    val data = dataDeferred.await()
    processData(data)
}

2.async/await:Deferred

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

async 有返回值,也就是Deferred,继承job,所有job有的它都有,还具备了job没有的携带数据回来的能力。
async 常用来运行异步耗时任务并且需要返回值的任务(eg:网络请求,数据库操作,文件读写等)
async 同样是CoroutineScope的一个扩展函数
async 方法同launch一样有三个参数:1.协程下上文;2.协程启动模式;3.协程体

Demo:
fun getDataAsync(): Deferred<Unit> {
    // 启动一个异步协程去执行耗时任务
    return GlobalScope.async {
        requestData()
    }
}

3.runBlocking:T

@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {

runBlocking 启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用。

Demo:
fun test() = runBlocking<Unit> {
    val job = GlobalScope.launch {
        delay(3000L)
        println("sub World!")
    }
}

7.CoroutineStart 启动模式

public enum class CoroutineStart {

    DEFAULT,
    
    LAZY,

    ATOMIC,

    @ExperimentalCoroutinesApi  // Since 1.0.0, no ETA on stability
    UNDISPATCHED;

    @InternalCoroutinesApi
    public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
        when (this) {
            DEFAULT -> block.startCoroutineCancellable(completion)
            ATOMIC -> block.startCoroutine(completion)
            UNDISPATCHED -> block.startCoroutineUndispatched(completion)
            LAZY -> Unit // will start lazily
        }

    @InternalCoroutinesApi
    public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
        when (this) {
            DEFAULT -> block.startCoroutineCancellable(receiver, completion)
            ATOMIC -> block.startCoroutine(receiver, completion)
            UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
            LAZY -> Unit // will start lazily
        }

    @InternalCoroutinesApi
    public val isLazy: Boolean get() = this === LAZY
}

作为构建器launch,async的一个参数,定义了 CoroutineBuilder 的执行 Coroutine 的时机,帮助我们应对各种使用场景。

有4种启动模式:

启动模式

作用

DEFAULT

默认的模式,立即执行协程体

LAZY

只有在需要的情况下运行

ATOMIC

立即执行协程体,但在开始运行之前无法取消

UNDISPATCHED

立即在当前线程执行协程体,直到第一个 suspend 调用

8.CoroutineContext

public interface CoroutineContext {

    public operator fun <E : Element> get(key: Key<E>): E?

    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

    public fun minusKey(key: Key<*>): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {

        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }
}

CoroutineContext是一个接口,本质上是一个类型安全的异构Map数据结构,是 Element 实例的集合。集合中每一个 Element 都有一个和对象引用相关的 key,作为 Element 的唯一标志。
作为kotlin协程的基本结构单元,利用好上下文至关重要:
Eg: 实现正确的线程行为,线程切换,拦截,声明周期,异常以及调试等

协程常用接口和类类图:

image

9.Job

public interface Job : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key<Job> {
        init {
            CoroutineExceptionHandler
        }
    }

    // ------------ state query ------------
    public val isActive: Boolean
    public val isCompleted: Boolean
    public val isCancelled: Boolean
    @InternalCoroutinesApi
    public fun getCancellationException(): CancellationException

    // ------------ state update ------------
    public fun start(): Boolean
    public fun cancel(cause: CancellationException? = null)

    // ------------ parent-child ------------
    public val children: Sequence<Job>
    @InternalCoroutinesApi
    public fun attachChild(child: ChildJob): ChildHandle

    // ------------ state waiting ------------
    public suspend fun join()

    ......
}

Job也是上下文元素,一种具有声明周期和任务层次的可被执行的协程模型。

Job实现关系是 Job <= Element <= CoroutineContext

  1. Job能够被组织成父子层次结构,并具有如下重要特性:
    1.1 父Job退出,所有子job会马上退出
    1.2 子job抛出除CancellationException(正常取消)意外的异常会导致父Job马上退出
  2. 类似Thread,一个Job可能存在多种状态,各状态转换关系如下:

image

注:直接使用launch获取到的job已经处于Active状态, Completing只是一个内部状态,外部观察还是Active状态,可以getCancellationException()查看主动还是异常退出

10.CoroutineScope

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

协程作用域—CoroutineScope,用于管理协程:

  1. 创建/启动/切换协程:它定义了launch、async、withContext等扩展函数,并在这些方法内定义了启动子协程时上下文的继承方式。
  2. 管理协程生命周期:它定义了cancel()方法,用于取消当前作用域,同时取消作用域内所有协程。

建议有生命周期的类继承 CoroutineSocpe,这样就能让全部协程跟着生命周期结束,统一管理。

Eg: 在UI逻辑类里面使用:

class CoroutinesActivity : BaseTestActivity(), CoroutineScope {

    var job: Job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_coroutine)
        findViewById<TextView>(R.id.tv_test5).setOnClickListener {
            loadDataAsync()
        }
    }

    private fun loadDataAsync() = launch {
        val ioData = async {
            // I/O operator
        }
        // do something else concurrently with I/O
        val data = ioData.await()
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
}

11.suspend

挂起函数:
用来修改函数的,表明函数是一个挂起函数,协程编译器会在编译期间进行CPS变换,用 suspend 修饰的函数,只能在协程体和同样使用 suspend 修饰的函数中调用。

原理:

  1. 挂起函数或挂起lambda表达式调用时,都会隐式传入一个Continuation类型的参数,这个参数封装了协程恢复后的执行的代码逻辑,类似回调接口。

    suspend fun requestData(): Bean { ... }

    Object requestData(Continuation cont) { ... }

  2. 协程内部实现不是使用普通回调的形式,而是使用状态机(状态模式)来处理不同的挂起点,大致的CPS(Continuation Passing Style)转化如下:

    正常代码逻辑:

    { suspend fun requestData(): Bean { ... } // 挂起函数 suspend fun createPost(bean: Bean, item: Item): Post { ... } // 挂起函数 fun processPost(post: Post) { ... } fun postItem(item: Item) { GlobalScope.launch { val token = requestData() val post = createPost(bean, item) processPost(post) } }

    // 编译后生成的内部类大致如下,创建协程以及协程挂起点都会生成一个case final class postItem$1 extends SuspendLambda ... { public final Object invokeSuspend(Object result) { ... switch (this.label) { case 0: this.label = 1; bean = requestData(this) break; case 1: this.label = 2; Bean bean = result; post = createPost(bean, this.item, this) break; case 2: Post post = result; processPost(post) break; } } } }

  3. 代码中每一个挂起点和初始挂起点对应的 Continuation 都会转化为一种状态,协程恢复实际上只是跳转到下一种状态中。

  4. 挂起函数将执行过程分为多个 Continuation 片段,并且利用状态机的方式保证各个片段是顺序执行的,所以挂起函数保证了协程内的顺序执行。

image

12.CPS状态机(Continuation Passing Style)

一般 coroutine 的实现大多基于某种运行时的 yield 机制,然而 kotlin 并没有这种下层支持,所以Kotlin在没有 yield 指令支持的 jvm 上基于 CPS 变换实现出了自己的 coroutine.

编译后每个 suspend 函数都会生成出一个实现了Continuation内部类StateMachine,用于保存函数的局部变量与执行点位。 StateMachine 替代 yield

  1. 记住执行点位;(switch case)每个 suspend 执行的位置对应一个 label,下次恢复执行时,按递增的 label 找到下一个执行位置。

  2. 记住函数暂停时刻的局部变量上下文。

    public interface Continuation { public val context: CoroutineContext public fun resumeWith(result: Result) } resume — 用于让已暂停的协程从其挂起或暂停处继续执行。

13.Suspend vs CoroutineScope

Kotlin中:

  1. 每一个声明为CoroutineScope的扩展方法的方法,都会马上返回,但是会并发地执行扩展方法指定的内容,这也是runBlocking不是CoroutineScope的扩展方法的原因之一。
  2. 每一个仅声明为suspend的方法,会等待其内部逻辑完成后再返回给调用者。

suspend方法就应该在所有任务都完成后再返回, 如果在suspend方法内部有需要并发执行的内容,那就应该等待他们都完成后再返回,此时可以使用coroutineScope{},而不是在方法签名上加上CoroutineScope扩展

14.ContinuationInterceptor

public interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
    public fun releaseInterceptedContinuation(continuation: Continuation<*>) {
        /* do nothing by default */
    }
    public override operator fun <E : CoroutineContext.Element> get(key: 
    ......
}

续体拦截器:也是上下文的一种
可以实现线程切换,,Hook,打日志等功能

我们自定义一个拦截器:

class MyContinuationInterceptor: ContinuationInterceptor {
    override val key = ContinuationInterceptor
    override fun <T> interceptContinuation(continuation: Continuation<T>) = MyContinuation(continuation)
}

class MyContinuation<T>(val continuation: Continuation<T>): Continuation<T> {
    override val context = continuation.context
    override fun resumeWith(result: Result<T>) {
        println("jason, result=$result" )
        continuation.resumeWith(result)
    }
}
GlobalScope.launch {
    launch(MyContinuationInterceptor() {
        val deferred = async {
            println(2)
        }
        val result = deferred.await()
        println("5. $result")
    }.join()
}

注:只能有一个拦截器,如果有多个后面的会覆盖前面的

14.CoroutineDispatcher

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
    public abstract fun dispatch(context: CoroutineContext, block: Runnable)
    @InternalCoroutinesApi
    public open fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = dispatch(context, block)
    public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
        DispatchedContinuation(this, continuation)
    @InternalCoroutinesApi
    public override fun releaseInterceptedContinuation(continuation: Continuation<*>) {
        (continuation as DispatchedContinuation<*>).reusableCancellableContinuation?.detachChild()
    }
    public operator fun plus(other: CoroutineDispatcher): CoroutineDispatcher = other
    ......
}

官方描述:协程上下文包含一个 协程调度器 ,它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

所有的协程构建器诸如 launch 和 async 接收一个可选的 CoroutineContext 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。

调度器本质上就是一个协程上下文的实现,同时实现了拦截器的接口,可以说调度器就是拦截器的一种,dispatch方法会在拦截器的方法 interceptContinuation 中调用,进而实现协程的调度。

val job = GlobalScope.launch(Dispatchers.Main + CoroutineName("Mycoroutine")) {
    for (i in 10 downTo 1) {
        hello.text = "Countdown $i ..."
        delay(500) // 等待半秒钟
    }
    hello.text = "Done!"
}

Kotlin有一下几种,我们一般使用官方提供的调度器

枚举值

作用

Main

UI线程

Default

共享后台线程池线程

IO

共享后台线程池线程

Unconfined

不确定,由被调用的线程或挂起函数来决定

// 也能使用这种方式简单的创建一个自定义的协程调度器
val myDispatcher= Executors.newSingleThreadExecutor{ r -> Thread(r, "MyThread") }.asCoroutineDispatcher()

15.withContext

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        // compute new context
        val oldContext = uCont.context
        val newContext = oldContext + context
        // always check for cancellation of new context
        newContext.checkCompletion()
        ......
        // SLOW PATH -- use new dispatcher
        val coroutine = DispatchedCoroutine(newContext, uCont)
        coroutine.initParentJob()
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    }
}

在kotlin中,这是一个很实用的函数,不创建新的协程,可以切换到指定的线程运行代码块,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。

coroutineScope.launch(Dispatchers.Main) { // 在 UI 线程开始
 
    // 切换到 IO 线程,并在执行完成后切回 UI 线程
    val image = withContext(Dispatchers.IO) {
        getImage(imageId) // 将会运行在 IO 线程
    }
    avatarIv.setImageBitmap(image) // 回到 UI 线程更新 UI
}

16.yield

对于yield开始的时候也是比较困惑,看了下注释:

Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines to run if possible.
如果可能的话,将当前协程调度器的线程(或线程池)返回给其他要运行的协程

对比JS

return the value,and continue when you next enter
在此处返回,并且在你下次进入时 从此处继续。

可以看到,Kotlin同样提供了yield语法,但是含义和其他语言的却不一样,此处仅仅是类似return,也就是暂时挂起了当前的协程,让其他协程运行,并且在实际的使用中还有别的细节和限制。

思考:使用场景? 比较消耗CPU资源并且持续性的任务

17.Channel

public fun <E> Channel(
    capacity: Int = RENDEZVOUS,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =
    when (capacity) {
        ......
    }

通道Channel提供了一种在流中传输值的方法。
用于多个协程之间进行数据相互传输,多个协程允许发送和接收同一个Channel的数据。

一个 Channel 是一个和 BlockingQueue非常相似的概念。其中一个不同是它代替了阻塞的 put 操作并提供了挂起的 send,还替代了阻塞的 take 操作并提供了挂起的 receive。

Demo:

val channel = Channel<Int>()

launch(IO) {
    // 这里可能是消耗大量 CPU 运算的异步逻辑,我们将仅仅做 5 次整数的平方并发送
    for (x in 1..5) channel.send(x * x)
}

// 这里我们打印了 5 次被接收的整数:
repeat(5) { println(channel.receive()) }
println("Done!")

18.Exception

public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineContext, Throwable) -> Unit): CoroutineExceptionHandler =
    object : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
        override fun handleException(context: CoroutineContext, exception: Throwable) =
            handler.invoke(context, exception)
    }

Kotlin协程的异常有两种:

  1. 因协程取消,协程内部suspend方法抛出的CancellationException 所有处理程序都会忽略这类异常,因此它们仅用作调试信息的额外来源,这些信息可以用 catch 块捕获。
  2. 常规异常,这类异常,有两种异常传播机制
    2.1 launch:将异常自动向父协程抛出,将会导致父协程退出
    2.1 async: 将异常暴露给用户(通过捕获deffer.await()抛出的异常)

我们可以通过try catch捕获,或者可以使用全局异常处理器CoroutineExceptionHandler,都很方便。

Demo:

fun exceptionTest() = runBlocking {

    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(deferred, job)
}

I/System.out: Caught java.lang.AssertionError

注:关于协程的初步研究,如果有疑问,可以贴出来讨论

参考文献:
Kotlin中文站,csdn: Kotlin协程详解