Kotlin学习笔记——(十二)协程上下文

注:编码工具为IntelliJ

目录

概念及作用

组成结构

Job

Defered

CoroutineDispatcher

CoroutineStart

CoroutineScope

协程作用域分类

协程上下文的使用

案例一:自定义协程上下文配置

案例二:分发器实践,向主线程分发任务

案例三:启动模式实践


概念及作用

        协程上下文是一个有索引的Element实例集合,每个element在这个集合里有一个唯一的key;

        协程上下文包含用户定义的一些数据集合,这些数据与协程密切相关;

        协程上下文用于控制线程行为、协程的生命周期、异常以及调试。

组成结构

        协程上下文结构图如下:

 Job:用于控制协程的声明周期。

CoroutineDispatcher:用于向合适的线程分发任务。

CoroutineName:协程名称,可以用于调试。

CoroutineExceptionHandler:用于处理未被捕获的异常。

public interface CoroutineContext {
    // 可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样 
    // 使用context[key] 这种中括号的形式来访问
    public operator fun <E : Element> get(key: Key<E>): E?

    // 和Collection.fold 扩展函数类似,提供遍历当前 context 中所有 Element 的能力
    public fun <R> 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<E : Element>

    public interface Element : CoroutineContext {...}
}

Job

        用于管理协程的生命周期。生命周期状态切换示意图如下:

                                          wait children
    +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
    | New | -----> | Active | ---------> | Completing  | -------> | Completed |
    +-----+        +--------+            +-------------+          +-----------+
                     |  cancel / fail       |
                     |     +----------------+
                     |     |
                     V     V
                 +------------+                           finish  +-----------+
                 | Cancelling | --------------------------------> | Cancelled |
                 +------------+                                   +-----------+

Job的各个方法的作用:

public interface Job : CoroutineContext.Element {
    // 调用该函数来启动这个 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()
}

Defered

        Defered继承自Job,功能与Job相同,区别是,Defered有一个await方法。

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

CoroutineDispatcher

        协程分发器,用于分发任务到合适的线程,类似于RxJava的Schedulers。有四种可选的调度器,如下所述:

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

        协程启动模式,类似于Android中Activity的启动模式。也有四种可选项,如下所述:

 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 都是 
// CroutineScope 的扩展函数,并且自动的继承了当前 Scope 的 coroutineContext分类及行为规则
//    官方框架在实现复合协程的过程中也提供了作用域,主要用以明确写成之间的父子关系,以及对于取消或 
//    者异常处理等方面的传播行为。该作用域包括以下三种:

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

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

//    coroutineScope 内部的异常会向上传播,子协程未捕获的异常会向上传递给父协程,任何一个子协程 
//    异常退出,会导致整体的退出
public suspend fun <R> 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 <R> 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)
    }
}

协程上下文的使用

案例一:自定义协程上下文配置

package step_thirteen

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import step_twelve.log

fun main() {
    var context = Job() + Dispatchers.Default + CoroutineName("customContext")
    log("context = $context")
    log("context[CoroutineName] = ${context[CoroutineName]}")
    context = context.minusKey(CoroutineName)
    log("context = $context")
}

输出:

[2021-11-25 11:39:03]-[main] context = [JobImpl{Active}@76fb509a, CoroutineName(customContext), Dispatchers.Default]
[2021-11-25 11:39:03]-[main] context[CoroutineName] = CoroutineName(customContext)
[2021-11-25 11:39:03]-[main] context = [JobImpl{Active}@76fb509a, Dispatchers.Default]

由于CoroutineContext中对+运算符进行了重载,所以上面代码中可以使用+运算符定义协程上下文对象。

案例二:分发器实践,向主线程分发任务

        在Android开发中尤其需要注意,由于协程可以嵌套,当最外层指定了主线程分发器,则内部所有代码可能都在主线程中执行,此时不宜做耗时操作,否则可能引发问题。一下代码在Android系统上运行。

GlobalScope.launch(context = Dispatchers.Main) {
    log("运行在主线程中的launch方式启动的协程")
}

输出:

2021-11-25 12:14:38.773 26916-26916/com.wcc.kt E/MainActivity: [2021-11-25 04:14:38]-[main] 运行在主线程中的launch方式启动的协程

案例三:启动模式实践

package step_thirteen

import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import step_twelve.log

fun main() {
    val coroutine = GlobalScope.launch(start = CoroutineStart.LAZY){
        log("调用start才会打印这句")
    }
    coroutine.start()
    // launch启动的协程是非阻塞的,所以需要主线程等待一会
    // Android中不需要,因为主线程一直阻塞,不会执行完毕
    Thread.sleep(100)
}

输出:

[2021-11-25 11:56:42]-[DefaultDispatcher-worker-1] 调用start才会打印这句

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值