协程的基本使用

kotlin 协程

一个系列

  1. 由浅入深的方式讲解
  2. 从协程基本使用

前提:

  1. 熟悉kotlin语言

  2. 对多线程,rxjava 并发编程 有一定的基础

  3. 什么是协程?

  4. kotlin 1.3开始引入的

  5. 协程的概念:1958 javascrip c# python ruby go lua 基于来自其他语言的既定概念

  6. Android平台上

    • 处理耗时任务 阻塞UI主线程 -> ANR
    • 保证主线程安全 确保安全的从主线程调用任何的suspend 函数

一. 协程的基本使用

  1. 创建协程,

    1.1 runBlocking
    runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
    CoroutineContext: 协程上下文
    suspend CoroutineScope.() -> T: 协程体
    返回参数: T

小结: 启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T

复制代码
1.2 GlobalScope.launch
CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
CoroutineStart:启动模式 
返回参数: Job

小结:启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用,返回值是一个Job

CoroutineScope: 协程作用域

//接下来的视频里面讲解

CoroutineScope & CoroutineContext ?

1.3 GlobalScope.async

CoroutineScope.async(

context: CoroutineContext = EmptyCoroutineContext,

start: CoroutineStart = CoroutineStart.DEFAULT,

block: suspend CoroutineScope.() -> T

): Deferred

返回参数: Deferred Deferred : Job

public suspend fun await(): T

高级用法(热数据通道Channel、冷数据流Flow...)

总结:启动协程 协程作用域范围

  1. runBlocking{} - 主要用于测试

    该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用。

  2. GlobalScope.launch/async{} - 不推荐使用

由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。

复制代码
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

额外的:

概念的补充说明:

CoroutineContext: 协程上下文

  1. 线程行为、生命周期、异常以及调试
  2. 包含用户定义的一些数据集合,这些数据与协程密切相关
  3. 它是一个有索引的 Element 实例集合,一个介于 set 和 map之间的数据结构。每个 element 在这个集合有一个唯一的 Key
  • Job: 控制协程的生命周期

  • CoroutineDispatcher: 向合适的线程分发任务

  • CoroutineName: 协程的名称,调试的时候很有用

  • CoroutineExceptionHandler: 处理未被捕捉的异常

// public interface CoroutineContext {

//可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样使用 context[key] 这种中括号的形式来访问

// public operator fun get(key: Key): E?

//和 Collection.fold 扩展函数类似,提供遍历当前 context 中所有 Element 的能力

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

//

//和 Set.plus 扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),

// 那么用+号右边的 Element 替代左边的。+ 运算符可以很容易的用于结合上下文,但是有一个很重要的事情需要小心 ------ 要注意它们结合的次序,因为这个 + 运算符是不对称的。

// public operator fun plus(context: CoroutineContext): CoroutineContext{...}

//

返回一个上下文,其中包含该上下文中的元素,但不包含具有指定key的元素。

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

//

// public interface Key

//

// public interface Element : CoroutineContext {...}

// }

复制代码
Job 负责管理协程的生命周期
 * ``
 *                                       wait children
 * +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
 * | New | -----> | Active | ---------> | Completing  | -------> | Completed |
 * +-----+        +--------+            +-------------+          +-----------+
 *                  |  cancel / fail       |
 *                  |     +----------------+
 *                  |     |
 *                  V     V
 *              +------------+                           finish  +-----------+
 *              | Cancelling | --------------------------------> | Cancelled |
 *              +------------+                                   +-----------+
 * ```
  调用该函数来启动这个 Coroutine,如果当前 Coroutine 还没有执行调用该函数返回 true,如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false
  public fun start(): Boolean
  通过可选的取消原因取消此作业。 原因可以用于指定错误消息或提供有关取消原因的其他详细信息,以进行调试。
  public fun cancel(): Unit = cancel(null)
   通过这个函数可以给 Job 设置一个完成通知,当 Job 执行完成的时候会同步执行这个通知函数。
  public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
  CompletionHandler 参数代表了 Job 是如何执行完成的。 cause 有下面三种情况:
//    -- 如果 Job 是正常执行完成的,则 cause 参数为 null
//    -- 如果 Job 是正常取消的,则 cause 参数为 CancellationException 对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
//    -- 其他情况表示 Job 执行失败了。
//    这个函数的返回值为 DisposableHandle 对象,如果不再需要监控 Job 的完成情况了, 则可以调用 DisposableHandle.dispose 函数来取消监听。如果 Job 已经执行完了, 则无需调用 dispose 函数了,会自动取消监听

join 函数和前面三个函数不同,这是一个 suspend 函数。所以只能在 Coroutine 内调用
这个函数会暂停当前所处的 Coroutine直到该Coroutine执行完成。所以 Job 函数一般用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。
//    当 Job 执行完成后, job.join 函数恢复,这个时候 job 这个任务已经处于完成状态了,而调用 job.join 的Coroutine还继续处于 activie 状态。
//    请注意,只有在其所有子级都完成后,作业才能完成
该函数的挂起是可以被取消的,并且始终检查调用的Coroutine的Job是否取消。如果在调用此挂起函数或将其挂起时,调用Coroutine的Job被取消或完成,则此函数将引发 CancellationException
public suspend fun join()


Deferred

public suspend fun await(): T  -》 Future
用来等待这个Coroutine执行完毕并返回结果。

suspend关键字

挂起点

CoroutineDispatcher:

// - Dispatchers.Default

// 默认的调度器,适合处理后台计算,是一个CPU密集型任务调度器。如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。注意它和IO共享线程池,只不过限制了最大并发数不同。

//

//

// - Dispatchers.IO

// 顾名思义这是用来执行阻塞 IO 操作的,是和Default共用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。

//

//

// - Dispatchers.Unconfined

// 由于Dispatchers.Unconfined未定义线程池,所以执行的时候默认在启动线程。遇到第一个挂起点,之后由调用resume的线程决定恢复协程的线程。

//

//

// - Dispatchers.Main:

// 指定执行的线程是主线程,在Android上就是UI线程·

// 由于子Coroutine 会继承父Coroutine 的 context,所以为了方便使用,我们一般会在 父Coroutine 上设定一个 Dispatcher,然后所有 子Coroutine 自动使用这个 Dispatcher

CoroutineStart:

// - CoroutineStart.DEFAULT:

// 协程创建后立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态

// 虽然是立即调度,但也有可能在执行前被取消

//

//

// - CoroutineStart.ATOMIC:

// 协程创建后立即开始调度,协程执行到第一个挂起点之前不响应取消

// 虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行

//

//

// - CoroutineStart.LAZY:

// 只要协程被需要时,包括主动调用该协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态

//

//

// - CoroutineStart.UNDISPATCHED:

// 协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点

// 是立即执行,因此协程一定会执行

CoroutineScope: 协程作用域

// public interface CoroutineScope {

// public val coroutineContext: CoroutineContext

// }

CoroutineScope 只是定义了一个新 Coroutine 的执行 Scope。每个 coroutine builder 都是 CoroutineScope 的扩展函数,并且自动的继承了当前 Scope 的 coroutineContext

分类及行为规则

// 官方框架在实现复合协程的过程中也提供了作用域,主要用以明确写成之间的父子关系,以及对于取消或者异常处理等方面的传播行为。该作用域包括以下三种:

// - 顶级作用域

// 没有父协程的协程所在的作用域为顶级作用域。

/ - 协同作用域

// 协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。

// coroutineScope 内部的异常会向上传播,子协程未捕获的异常会向上传递给父协程,任何一个子协程异常退出,会导致整体的退出

// public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R {

// contract {

// callsInPlace(block, InvocationKind.EXACTLY_ONCE)

// }

// return suspendCoroutineUninterceptedOrReturn { uCont ->

// val coroutine = ScopeCoroutine(uCont.context, uCont)

// coroutine.startUndispatchedOrReturn(coroutine, block)

// }

// }

// - 主从作用域

// 与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。

// supervisorScope属于主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程

// public suspend fun supervisorScope(block: suspend CoroutineScope.() -> R): R {

// contract {

// callsInPlace(block, InvocationKind.EXACTLY_ONCE)

// }

// return suspendCoroutineUninterceptedOrReturn { uCont ->

// val coroutine = SupervisorCoroutine(uCont.context, uCont)

// coroutine.startUndispatchedOrReturn(coroutine, block)

// }

// }

//

二. 在Android中怎么用?

CoroutineScope

CoroutineDispatcher

jetpack + 协程

viewmodel

lifecycle

基本操作

协程作用域 & 上下文 结构化并发

suspend到底是什么?

协程高级 异常,chanel 多线程临界资源访问

异常处理

java : 受检查的异常 Checked Exception(CE)

kotlin: 没有 CE C#

kotlin 协程异常处理

从返回值来看

  1. job: try{}catch{} CoroutineExceptionHandler 有效 设置在根作用域有效
    协同: 设置在根作用域有效 子协程发生了异常,所有的协程都会取消掉

异常消费:

复制代码
2. Defferred:                                     无效
y {
            deferred.await()
        } catch (e: Exception) {
            e.printStackTrace()
        }

private val handlers: List<CoroutineExceptionHandler> = ServiceLoader.load(
        CoroutineExceptionHandler::class.java,
        CoroutineExceptionHandler::class.java.classLoader
).iterator().asSequence().toList()
JVM的ServerLoader 机制

//总结:

//1. 【协同】协程中异常传播是双向的,子协程的异常会导致父协程结束,父协程的异常也会导致整个结束

//24. 【主从】SupervisorJob的异常是单向传播的,子协程的异常不影响其他,父协程的异常会影响全局,因此,SupervisorJob的子协程都应该设置捕获异常句柄

复制代码
  //注意: 亲儿子: 单向传播, 孙子://【协同】
  val scope = MainScope()//主从
  scope.launch{亲儿子 //协同
     launch{
	 
	 }
	  launch{
	 }
  }
  1. Job的异常需要在根协程域捕获,类似 Thread.defaultUncaughtExceptionHandler,在子协程无法捕获,如果不捕获,会崩溃
  2. Defferd的异常依赖用户来最终消费异常,如果没消费,不需要捕获,也不会崩溃,如果消费了则需要捕获,不然会崩溃

挂起函数

suspend 到底是什么? 就是回调

续体 + 状态机模式

suspend 变成一个协程 不会阻塞线程?

suspend 提供了这样一个约定(Convention):调用这个函数不会阻塞当前调用的线程

作用域 上下文

复制代码
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
}

不同的使用角度来看

作用域:管理协程

协同:发生了异常,异常如何传播 双向传播 - 一荣俱荣一损俱损

主从:单向传播 父亲出问题了 ,儿子都完了, 儿子出事了,不影响父亲以及兄弟姐妹

上下文: 记录了协程的运行环境[name,线程,job]

Activity start thread 干它的事情去了, 什么时候干完,有没有出什么问题 start thread

黑盒性质: 一个入口,一个出口

结构化并发: 满足这个黑盒性质