Kotlin Coroutines ABC(概览)

·  阅读 432
Kotlin Coroutines ABC(概览)

序言

  • 你可能对Coroutines一无所知,需要一个入门引导;
  • 也可能只是用过,但不了解为什么用,它到底是什么;
  • 又或者是对Coroutines有一定了解,但想要探索更全面的设计思想 和 底层原理;

本文希望开启Coroutines的探索大门:从入门引导开始(本文内容),到最佳实践,再深入到原理实现分析,希望做一个较为全面的Coroutines系列讲解(后续)

希望与处于各个阶段的你一同探索,更加深入的了解Coroutines,以便在工作中更得心应手的使用它来提升工作效率,也能从它的设计中获得一些启发😄。

本文简介

本文将进行Coroutines的重点知识全局引(po)导(bing),源码分析点到为止. 主要讲:Coroutines的设计由来、本质、核心概念、基本用法. 希望能够帮助大家开启揭开Coroutines的神秘面纱之旅.

至于其中的**工作最佳实践原理&源码分析**,在文章中涉及到的地方会留有PlaceHolder,会计划在后续的文章中进行深入探讨.

为什么使用Coroutines🤔?

一来就塞个东西给你,说:“用它,好用,我教你”. 这样的方式,会让人感到很难受,即便是勉强接受,心里也会嘀咕:凭什么?

其实,就是想问:你这个东西它能给我解决什么问题?

解决了什么问题(Core competence)?

Google I/O 怎么说(at 5min:14s): Coroutines simplify async code by replacing callbacks.

在异步编程把令人头疼(🤯)的Callbacks去掉,就是Coroutines核心想解决的问题。

就这么简单的一件事儿?对,核心就这么看起来“简单”的一件事儿,但痛点绝对是足够了... 那么问题来了,告诉我为什么是痛点?and Coroutines是怎么解决这个问题的?

磨刀不误砍柴工,既然是解决异步编程的问题,不妨先简单回顾一下 同步编程 和 异步编程 是怎么工作的:

  • Sync Code: 同步执行等待结果,代码从上往下按照顺序执行,符合人类的认知(串行),简单易懂.
    • Happy code style😊.

  • Async Code: 通过Callback实现异步线程的执行结果回调,当回调嵌套变多 + 方法变多时,代码可读性 和 异步编程的迭代和管理难度 就会指数上升(痛点就来了,仔细品).
    • Struggling code smell🤯.

这是从同步\异步编程的维度上,来看我们平常解决问题的两种代码形式. 我们每天都跟它们打交道,可以说是非常熟悉了.

现在请花几分钟,仔细回顾一下你的编程历史,想想下面的两个问题:

  • 当我们有同步执行需求的时候,我们是否是:乐于 并且 善于 写Sync Code(Happy top-down code style😊)

  • 当我们有异步执行需求的时候,我们是否是:无奈 并且 别扭的 写Async Code(Struggling callback code style🤯);

千呼万唤始出来,我们现在可以来看Coroutines 是怎么 replace callbacks了. (聪明伶俐的你可能已经想到了💡).

  • 解决方案💡: 用快乐的同步编程写法(Happy code style),来解决难受的异步编程问题(Struggling code style).

  • Q1: networkRequest是异步执行的吗?

    • 是的. 异步执行,不阻塞当前执行的主线程. 当Request执行完成之后结果返回,主线程继续执行.
  • Q2: 没有了callback,有什么魔法🪄?

    • 很简单,就一个关键字: suspend🪄. 那suspend又是什么? 见后文核心概念: Suspend

附加对比案例(必看):

如果你觉得写callbacks成本也不大?嗯,那可能被callbacks毒打的还不够,需要丢个王炸出来💣.

无可厚非,每一次新的变革到来之前,我们都会习惯性将旧的规则当作信仰,直到先驱者为他们打开新大陆.

想一想:当Callbacks方法变多 + 嵌套层数变多的时候,耳熟能详的“回调地狱”就出来了👇:



小结: 替换Callback后带给我们什么好处呢?

  • 异步编程成本变低了: 用快乐的同步编程写法(Happy top-down code style),来解决难受的异步编程问题(Struggling callback code style).

  • 冗余的代码量变少了: 减少了一堆由于callbacks 带来的冗余代码,阅读起来更容易理解.

    • 由于callbacks的可读性复杂导致的人工代码问题可能性会减少.****

    • 多人协作的持续迭代的产品项目里,代码可读性永远是第一名.

Coroutines优势(More than that)?

Coroutines核心是替换callbacks,起于此但不止于此(more than replacing callbacks). 为了解决好这个问题,它还衍生了有很多优势特性:一切都为让开发者能够写异步编程更舒适.

下面来简单介绍它的优势特性:

Non-blocking

上一节已经说到:用同步非阻塞的代码方式,解决异步编程问题;可在suspend点挂起,且不会阻塞当前线程的执行,这是它的最大卖点(核心优势).

  • 举例时刻🌰: 上面的例子很深刻了,就再不多举例了.
  • 问题时刻🤔: 协程suspend如何做到非阻塞式的异步编程,有什么新的技术吗🪄?

    • 回答:哪有什么岁月静好,只是有人替你负重前行. 具体请见下文: Suspend

Structured Concurrency

关键目标:并发任务可管理,不是发出去就算了。

什么是结构化并发模式?

  • 如果用一句话概括,那就是:即使是进行并发的操作,也要保证控制流路径的单一入口和单一出口程序可以产生多个控制流(多个协程)来实现并发但是所有的并发路径在出口时都应该处于完成 (或取消) 状态,并合并到一起。

Coroutine在语言级别遵循结构化并发编程模式,内置协程任务作用域(CoroutineScope),统一管理(取消\异常处理)Scope内运行的协程,降低并发编程的成本. 具体功能体现在:

  • 协程启动: 协程作用域负责执行和启动协程(子协程)。
  • 协程管理:协程作用域会等待所有子协程完成才会标记为完成运行。子协程的生命周期依附于其父协程作用域的生命周期。
  • 取消协程: 如果协程执行出现问题 或者 用户想要撤销协程任务操作,可以通过作用域取消其下的所有运行的协程(包括子协程)。
  • 举例时刻🌰:
    • 见下文:
      • 如何启动一个协程
      • 如何取消正在执行的协程
      • 如何捕获协程抛出的异常
  • 问题时刻🤔:Coroutines是如何实现并发结构化编程的?
    • 回答: 通过定义CoroutineScope + CoroutineContext来完成协程的追踪 和 生命周期统一管理. 具体解析见下文 CoroutineScope + CoroutineContext.

Lightweight

Kotlin的协程是语言层级的构造 和 资源分配(可看作一种形式的控制流),并不涉及操作系统侧的资源分配. 所以,在使用上对比线程来说是比较轻量的(你创建协程的时候,心里负担远没有线程那么大).

  • 举例时刻🌰:
val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

fun main() = coroutineScope.launch {
    repeat(100_100) { //重复启动大量的协程
        launch { //coroutine, like a runable code block
            delay(5000L) //suspend point 1
            print("suspend point 1 resumed")

            delay(5000L) //suspend point 2
            print("suspend point 2 resumed")
        }
    }
}

你可以创建十万个协程来执行你的任务,但是你的程序却并不会崩溃 (如果是线程,你可以试试看).

  • 问题时刻🤔: 那么Kotlin协程能够提高线程执行的效率吗?或者换句话说,能够提升吞吐量吗?

Official extensions Support

Jetpack官方对各种库都支持了丰富的Coroutines的extensions工具,在Android体系下开发协程简单,易上手;

  • 问题时刻🤔: 这官方会实时更新和维护吗?
    • 官方正在大力推广Kotlin和Coroutines,应用层代码大都要用Kotlin和Coroutines来写. 所以,基本上不用担心不会维护.

Coroutines 本质是什么🤔?

上面解释我们为什么使用Coroutines。但是从代码库扒下来一看,那么的复杂,各种官方推销的魔法🪄概念五花八门,神乎其神。感觉像是被蒙上了一层神秘的面纱。

从这节开始,我们来慢慢揭开它的面纱,看看它的真面貌。

Kotlin 官网 怎么说?

It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code.

Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads.

概念类似线程,轻量级的线程(个人觉得这样有点不太恰当,可能会误导大家). 经过上面的一些简单例子说明,我们或多或少也发现,协程是语言层面的一套概念,底层其实也还是要依赖线程来作为执行环境.

Android官网 怎么说?

A coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously.

一种并发设计模式,简化异步编程. 这里强调的是并发设计模式,Kotlin并没有引入新的底层技术点,而是一种并发的设计模式的实现. (个人非常支持这种说法,所以Kotlin还得是要靠Google来发扬光大😄)

我们应该怎么理解?

官方说的总感觉有点模糊,似懂非懂。

Coroutines到底本质是什么,我们可以分两部分来看:视觉认知底层本质.

  • 首先,从视觉认知上,看看Coroutines外表长什么样?

协程视觉认知就是launch作用域下的一整块代码,跟Runnable非常相像. 这点我非常赞同Google Coroutines推广者 Manuel Vivo 在 协程的介绍 视频里(at 7min: 26s) 对 Coroutines 的描述:

Coroutines is a runnable block code with superpower. Takes a block of code run in a thread.

这里的superpower指的是挂起(suspend). 因为有了它,才实现了在suspend方法处实现挂起. 才赋予Coroutines通过 同步非阻塞的代码 解决 异步并发编程问题 的核心能力.

  • 其次,从底层本质上,再看Coroutines到底是什么?

一句话描述:协程是kotlin官方推出的一个语言级别的轻量&高级线程池封装库,用来简化开发者异步编程的成本,使得并发编程易读、易写、易管理 . (请记住,默念十遍)

其实跟React编程思想原理差不多:基于某种设计模式,将线程封装起来并提供简单易用的API给开发人员使用。换句话说,Kotlin的协程就是另外一种风格的RxJava(但比RxJava用起来方便些)。

Coroutines的核心概念♨️?

如上所述,Coroutines是一套语言级别的高级线程池封装库,内部有非常复杂的设计. 但是究其根源,核心的概念有三:Suspend(最核心)、CoroutineScope、CoroutineContext.

下面,我们来看下它们分别起到什么作用:

Suspend

Suspend 是 Coroutines 解决 核心问题(replace callbacks)的 核心武器。

顾名思义,suspend的意思是挂起(也有翻译成暂停的). 官方一点的说法,它具体包括两个方面:

  • 挂起(suspend): 遇到挂起点(suspend function)的时候,把执行的协程上下文挂起,以便线程可以去执行别的协程任务;
  • 恢复(resume): 当挂起点(suspend function)执行完成之后,再恢复该协程上下文并执行剩余代码;

划重点: Suspend挂起(阻塞)的是协程,恢复的也是协程. 所以,它并不阻塞线程的执行(是有一些前提的,继续看).

Q1: Suspend是如何实现这种魔法🪄?

上文有说到:Coroutines的本质是一种并发设计模式,是一个语言级别支持的标准高级 线程 封装库. 既然是语言级别的封装,那就是新瓶装旧酒.

其实,并不是真的不需要Callbacks,而是 Kotlin 在语言层面帮我们实现了, 开发者不需要写Callbacks了。如下示意图👇:

对应Kotlin实现的(编译器生成的)代码比较复杂:Suspend的魔法🪄揭秘 —— Show me code (实在看不了)

简化之后保留核心的代码,大致长这样:

//编译器给我们生成了Continuaction这个类,并且作为方法参数. 
//此处应该有:“啊哈“时刻.
fun loadData(continuation: Continuation<Any?>) {
    //continuation式回调
    val continuation = continuation ?: object : ContinuationImpl {
        // result保存当前结果
        val result: Object 
        
        // label指示状态机下一个状态
        val label: Int

        // resume方法将恢复挂起协程的执行
        // 被内部的调度器调用,执行协程代码
        fun invokeSuspend(Object $result) {
            this.result = con.result
            this.label = con.label
            loadData(this)
        }
    }

    //有限状态机
    when(continuation.label) {
        0 -> {
            //set flag to next suspend point state
            completion.label = 1 
            //execute current suspend point code
            //传入了completion作为当该方法执行完成之后的回调
            networkRequest(completion)
        }

        1 -> {
            //执行最后一段方法
            show(data)
        }

        else -> {
            throw IllegalStateException(...)
        }
    }
}

到这里,不禁感慨一下:哪有什么岁月静好,只是有人替你负重前行.

划重点:从上面的反编译例子我们可以得出,一个调用了N个suspend function的方法,被suspend point分割成 N + 1个有限状态机;因此,suspend的恢复/取消也就只能在suspend point的方法粒度.

灵魂追问🤔: 完整的Suspend代码的生成 和 实现原理是怎样的?

  • 编译器遵循CPS(Continuation - passing - style)的模式进行转换,CPS的本质是:有限状态机 + Continuation. 我会理解成:优雅版Callback.

Q2: 那什么时候应该使用Suspend呢?

记住,当一个方法被Suspend标记的时候,表示 开发者 在告诉 Kotlin编译器:这是一个耗时的方法,请帮我把它转变成callbacks的执行方式.

仔细品一品这句话会发现:并没有涉及到线程的切换,Kotlin编译器也不会帮我们做线程的切换,它仅仅是把同步代码 转换成了 CPS式 的回调代码 (比我们写的要优雅一些)!

所以,请稳住别浪,Suspend不是永恒的魔法🪄.

那为什么官网说Suspend 是 Main-Safety的,不阻塞UI线程的执行?

其实官网也没有说错,只是大家对Main-Safety理解的角度不一样. 在官方的协程体系里,Suspend是一个通用约定规范,主要体现在两点:

  • 对于编译器: 这是一个可挂起的方法,需要把同步代码 转换 生成CPS模式的回调代码.
  • 对于开发者: 这是一个可能耗时的方法,需要调用Kotlin提供的协程调度器,并且要自己主动切换到异步线程里执行.

所以,不是所有方法都要声明成suspend,只有耗时方法 或者 IO方法 才需要声明;同时,声明Suspend的方法如果耗时,要负责切换到异步线程里执行,否则该阻塞还是会阻塞(no zuo no die) .

灵魂拷问🤔 为什么官方不能直接帮我们生成线程切换的代码?

个人猜测:

  • 编译器并不能准确的知道业务方要在哪个suspend点做线程切换调度,切换的策略配置是什么;这是业务要求很灵活的地方,其实不好固化(不然Java也不会暴露Thread让大家自己调用了);
  • 不能确定的事儿,不适合交给机器去做,交给能确定的人去做比较好(否则程序员也没有价值了);

CoroutineScope

上面讲Structured Concurrency的时候其实已经讲到,CoroutineScope其实是结构化并发编程模式下在协程里的实现。理解了结构化并发编程,也就理解了CoroutineScope.

同样顾名思义,CoroutineScope就是: Kotlin 语言级别标准库层面给Coroutine设定了一个边界,或者说影响范围. 规定Coroutine只能在Scope里面产生 和 执行,以此达到统一取消,以及处理协程异常 的 结构化并发编程目标.

我们来看一个Scope取消协程的例子🌰:

class CoroutinesUseCases {
    val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    val job1: Job? = null

    fun launchCoroutine() {
        //通过Scope启动一个协程,会得到该协程任务所对应的Job.
        job1 = coroutineScope.launch {// New coroutine
            loadData()
        }

        job2 = coroutineScope.launch {// New coroutine
            loadData()
        }
    }

    fun cancelCoroutine() {
       //取消当前的协程任务,会及联的取消其子协程;如何及联,见下文[CoroutineContext]
       //注意⚠️:对CoroutineScope没有影响,对该Scope下启动的兄弟Job2也没有影响.
       job1?.cancel()
       job2?.cancel()

       //通过CoroutineScope,取消所有的子协程; 具体原因,见下文[CoroutineContext]
       coroutineScope.cancel()
    }
}

Q1:CoroutineScope如何做到管理内部的协程?

先看一下CoroutineScope的接口定义:仅包含一个协程上下文的参数.

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
     
    public val coroutineContext: CoroutineContext

}

So,CoroutineScope就这?如此简单?

  • 从设计上讲,"挺简单".
    • CoroutineScope是一个结构化并发编程规范的具象化. 它主要包含接口约束 + 协程执行上下文. 通过协程的上下文,找到Scope下所有执行的协程,完成管理协程的目标.
  • 从实现上讲,"不简单".
    • 下图是实现CoroutineScope的类. 我们可以看到所有类型的协程 都实现了该接口规范.有没有一点熟悉?类似ViewGroup 和 View的关系. 协程本身其实就是一个CoroutineScope!

其实我们会发现,实际上CoroutineScope它确实什么事情也没做,具体负责协程的调度 和 取消实现是里面的CoroutineContext. 而CoroutineScope的存在的意义是它所定义的接口约束和规范.

CoroutineContext

顾名思义,CoroutineContext其实是:一个存储协程执行所需的一系列上下文环境信息的集合. 这些上下文信息主要包括下面四个(不止这些),而且它们都是CoroutineContext:

  • Job:控制当前协程的执行生命周期:开始、取消、join.
  • CoroutineDispatcher: 负责调度分配协程在哪个线程中执行.
  • CoroutineName:协程的名字,用来Debuging.
  • CoroutineExceptionHandler:用来处理协程抛出的异常.

我们来看一个使用例子🌰:及联取消.

val coroutineScope = CoroutineScope(Dispatchers.Main 
                                    + Job() 
                                    + CoroutineName("job"))
val scopedJob: Job? = null

fun startCoroutines() {
   //启动一个协程,获得当前协程的Job执行上下文.
   scopedJob = coroutineScope.launch {
       //case1: 新起一个内部子Scope,通过plus的操作符 "继承" 外部scope的context.
       CoroutineScope(this.coroutineContext + Dispatchers.IO).launch {
           //do something
       }
       
       //case2: 新起一个内部子协程,launch方法内部自动继承当前Scope的context.
       launch {
           //do something
       }
   }
   
   //再启动一个新的协程
   coroutineScope.launch {
       //case3: 新起一个内部子协程,launch方法内部自动继承当前Scope的context.
       launch {
           //do something
       }
   }
}

fun cancelAllCoroutines() {
    //方式一:通过外层的Coroutine产生的Job来完成及联取消, case1、case2会被取消.
    //       因为内部两个协程均关联外层的coroutineContext.
    scopedJob?.cancel()
    
    //方式二:通过外层Scope取消,case1、case2、case3都会被取消.
    coroutineScope.cancel()
}

我们再看一个异常的案例:异常传播 .

val coroutineScope = CoroutineScope(Dispatchers.IO)

suspend fun main() {
    val job = coroutineScope.launch {
        launch {
            println("开始执行第一个协程")
            //do something
            delay(5000L)
            println("这段代码得不到执行,因为下面的Coroutine抛出来的异常将会取消Scope里所有的协程")
        }
        
        launch {
            println("开始执行第二个协程")
            //do something
            delay(5000L)
            println("这段代码得不到执行,因为下面的Coroutine抛出来的异常将会取消Scope里所有的协程")
        }
    }
    
    coroutineScope.launch {
        print("开始执行第三个协程")
        //do something
        delay(3000L)
        print("第三个协程执行完成,遇到异常了!😡")
        throw IllegalStateException("i am crashed, and nobody catch me!")
    }
   
    job.join()
}

背后的故事: 如果某个Child异常,则会将异常先传播到Parent,然后Parent取消其他兄弟协程,然后再取消自己,然后再往父协程传播,达到结构化并发的 及联取消 目的.

  • 在协程里,这个机制这叫做:Exception Propagation.
  • 核心代码:

  • 动态图解:

具体代码分析,敬请期待:Kotlin Coroutines Solutions — Cancelation & Exception Handle(WIP)

下文还有其他通过Scope协作处理协程异常 和 取消的例子🌰:

  • 见下文:
    • 如何捕获协程抛出的异常
    • 如何取消正在执行的协程

Q1:CoroutineContext是如何设计使得它能够追踪内部的协程执行?

我们来看一下CoroutineContext的接口设计:它提供context之间的 +-操作,两个context可以合并 或者 相减,同时提供get查询操作. 这种能够combine合并的特性,官方称之为indexed set(set 和 map的混合体). 这也正是它的强大和精髓之处.

public interface CoroutineContext {
    //返回一个CombinedContext
    public operator fun plus(context: CoroutineContext): CoroutineContext
    
    public fun minusKey(key: Key<*>): CoroutineContext
    
    public operator fun <E : Element> get(key: Key<E>): E?
    
    public interface Element : CoroutineContext {
        public val key: Key<*>
    }
}

//看倒这里,就有我们熟悉的链表的影子了.
internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
}

本节开头所说的Job\CoroutineDispatchers\CoroutineName\CoroutineExceptionHandler都是继承自Element(也即CoroutineContext).

正因如此的设计,才使得CoroutineContext元素能够累加(同层协程不同元素累加跨层协程的元素合并). 简单理解可以认为是形成了一个链表,只需要得到表头,就能够找到所有的元素,对他们进行操作.

至此,再回头再看看上面的 及联取消代码,我们至少也可以从设计上解释 及联取消 是怎么做到了:

scopedJob = coroutineScope.launch {
    //case1: 新起一个内部子Scope,通过plus的操作符 继承外部scope的context.
    CoroutineScope(this.coroutineContext + Dispatchers.IO).launch {
       //do something
    }
}
  • 设计解释:
    • Coroutine是CoroutineScope,Job是coroutineContext.
    • 外层Coroutine的Job取消 = 该Coroutine Scope取消.
    • 而内部的Coroutine(Scope)关联了外部的coroutineContext (通过plus操作符,进行Combine).
    • 所以Coroutine Scope取消则会将所有的内部关联的Coroutine取消.

问题时刻🤔: 异常传播、协程调度也都是CoroutineContext的实现,他们的具体代码实现原理是什么?

Coroutines的基本用例🌰?

本节仅举一些非常基本的用例,作为入门理解;在实际开发中,会有更加复杂的使用场景 和 处理方案,里面会涉及更多的机制 和 原理. 这些内容会在 ****[最佳实践(待续)] ****和 [原理分析(待续)] 作为进阶学习进行探讨

Q1: 如何启动一个Coroutines?

Kotlin官方提供了以下两种方式来启动协程:

  • launch(推荐): 启动一个新的协程而不将结果返回给调用方,任何被视为“一劳永逸”(one shot)的工作都可以使用 launch 来启动。即,无结果预期的启动协程.
  • async: 启动一个新的协程并返回Deferred对象(类似Java的Future),在需要获取结果的地方,调用await的挂起函数等待结果返回。即,有结果预期的启动协程.

注意⚠️: async和launch在处理异常Exception的机制不一样:

  • launch启动的协程,内部异常发生会直接在launch代码块抛出,你可以直接try catch;
  • async启动的协程,内部抛出异常不会在async代码块抛出(意味着你try catch不了async),直到调用await的时候才会抛出.

因此,使用async可能会有一些“静默异常”你无法追踪到,不会出现在崩溃指标中,也不会在logcat中输出. 因此,一般推荐使用launch. 具体原因见: 如何捕获协程抛出的异常

我们来简单看一下用例:

  • launch: 多个launch并行执行
//谨记:所有的协程启动,都需要在CoroutineScope中完成.
val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    
fun loadDataWithLaunch() {
    //启动一个新的协程,不会阻塞当前代码,继续往下执行
    coroutineScope.launch {
        loadData1()
    }
    
    //立马启动一个新的协程,不需要等待上面协程执行完成.     
    coroutineScope.launch {
        loadData2()
    }
}
  • async: 多个async并行执行
//谨记:所有的协程启动,都需要在CoroutineScope中完成.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())

suspend fun loadDataWithAsync() {
    val deferredOne = coroutineScope.async { 
        val result = loadData1()
        result
    }
    
    val deferredTwo = async { 
        val result = loadData2()
        result
    }
    
    //block to get result. exception, if happened, will be throwed at here.
    deferredOne.await()
    deferredTwo.await()
}

Q2: 如何取消正在执行的协程?

注意⚠️: 取消的粒度是协程内部调用的suspend function. 也即,最快也要等到当前执行的suspend完成后,才能在resume的时候检测取消.

通过CoroutineScope来启动协程,所有在Scope下的协程都将统一收到Scope的管理. 遵循结构化并发编程的范式下,可以统一取消内部所有的协程任务.

val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var scopedJob: Job? = null

fun onCreate() {
    scopedJob = coroutineScope.launch {
        //type1:新起一个协程,使用scope的线程调度器
        launch {
            //doSomething
            suspendFunction1()
            suspendFunction2()
        }
        
        //type2: 新起一个协程,并切换线程调度器
        launch(Dispatchers.IO) {
            //doSomething
            suspendFunction1()
            suspendFunction2()
        }
        
        //type3: 新起一个协程,并且切换线程调度器,并且要挂起等待执行结果
        withContext(Dispatchers.IO) {
            //doSomething
            suspendFunction1()
            suspendFunction2()
        }

        //type4: 非suspend function中新起一个Scope,
        //       并且切换线程调度器到Dispatchers.IO. 
        nonSuspendFunction(coroutineContext)
    }
}


fun nonSuspendFunction(coroutinesContext: CoroutineContext) {
    CoroutineScope(coroutineContext + Dispatchers.IO) {
         //doSomething
         suspendFunction1()
         suspendFunction2()
    }
}

fun onDestroy() {
    //页面销毁的时候调用cancel,则上述4种type下执行的协程任务都会在
    //suspend的point点被取消. 
    scopedJob?.cancel()
}

关于更多协程取消的最佳使用案例,敬请期待:Kotlin Coroutines Solutions — Cancelation & Exception Handle(WIP)

Q3: 如何捕获协程抛出的异常?

在协程里捕获异常,有两种方式:

  • Try catch(推荐) 一般性正常思维,在执行的代码中使用try catch的方式.
  • CoroutineExceptionHandler: 通过往Scope中注入Handler处理那些没有try catch的异常。平常业务编程中尽量不要自己使用,除非你对异常传播机制比较了解,且有明确的场景要用到.

    • 用途举例:协程的稳定性指标监控.

我们来看一下使用案例吧🌰:

  • try catch:
val coroutineScope = CoroutineScope(Dispatchers.IO)

fun onCreate() {
    //case1(推荐)协程内部的局部代码try catch.
    coroutineScope.launch {
        //do something
        try {
            networkRequest()
        } catch(t: Throwable) {
            //handle exception here.
        }
        
        //do another thing
        showResult()
    }
    
    //case2:对launch协程进行整体try catch.
    try {
        coroutineScope.launch {
            //do something
            delay(5000L)
            throw IllegalStateException("i am crashed, and nobody catch me!")
        }
    } catch(t: Throwable) {
        //catch exception throwed from launch coroutine
    }
    
    //case3:对asyn的协程进行try catch
    //错误方式:❌
    try {
        coroutineScope.async {
            //do something
            delay(5000L)
            throw IllegalStateException("i am crashed, and nobody catch me!")
        }
    } catch (t: Throwable) {
        //我们无法在这里捕捉到异常. 
    }
    
    //正确方式:✅
    val defferedResult = coroutineScope.async {
        //do something
        delay(5000L)
        throw IllegalStateException("i am crashed, and nobody catch me!")
    }
    
    try {
        //在await执行的时候才会抛出来,在此前都会被存放在Coroutine内部中.
        defferedResult.await()
    } catch(t: Throwable) {
        //我们在这里便能够捕捉到async里抛出的异常.
    }
}
  • CoroutineExceptionHandler:
//当我们往Scope中设置CoroutineExceptionHandler之后,就能捕获未处理的异常.
val coroutineScope = CoroutineScope(Dispatchers.IO
                                    + CoroutineExceptionHandler { coroutineContext, throwable ->                                      
                                    
    print("we get an exception, thread will still running, message is: ${throwable.message}")
})


fun onCreate() {
    scopedJob = coroutineScope.launch {
        //do something
        delay(5000L)
        throw IllegalStateException("i am crashed!")
    }
}

关于更多协程异常处理的最佳使用案例:

Q4: 如何切换协程的执行线程?

在协程中,切换线程的方式有几种:launch切换、async切换、withContext切换(推荐) .

我们来看一下使用例子:

  • launch切换: 在协程执行作用域内,通过launch新起一个协程,并切换到其他线程执行.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var scopedJob: Job? = null

fun onCreate() {
    scopedJob = coroutineScope.launch {    
        //切换到IO线程,store db
        launch(Dispatchers.IO) {
            //doSomething
            suspendFunction1()
            suspendFunction2()
        }
    }
}
  • async切换: 在协程执行作用域内,通过async新起一个协程,并切换到其他线程执行.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var scopedJob: Job? = null

fun onCreate() {
    scopedJob = coroutineScope.launch {
        //切换到IO线程
        val deffered = async(Dispatchers.IO) {
            //doSomething
            suspendFunction1()
            suspendFunction2()
        }

        try {
            //等待执行结果,并catch可能发生的异常
            deffered.await()
        } catch(t: Throwable) {
            //handle exception here.
        }
    }
}
  • withContext切换(推荐) 在当前协程里,切换到其他线程执行,并且能够挂起等待内部的返回结果. withContext消耗很小,比起launch和async的切换方式高效很多!
    • withContext是一个suspend方法,不阻塞当前线程.
val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
var scopedJob: Job? = null

fun onCreate() {
    scopedJob = coroutineScope.launch {
        //切换到IO线程,并等待执行结果. 
        val result = withContext(Dispatchers.IO) {
            val result = loadData()
            return result
        }
        
        show(result)
    }
}

后续相关系列

最佳实践

原理分析

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改