Kotlin中的协程 - CoroutineContext

前言

Kotlin是一种在Java虚拟机上运行的静态类型编程语言,被称之为Android世界的Swift,在GoogleI/O2017中,Google宣布Kotlin成为Android官方开发语言

回顾

Kotlin中的协程(一) 中我们探讨了以下问题

协程的特点
协程的介绍
协程中的一些元素概念
如何创建一个协程
协程不同的调度模式
协程不同的执行模式
非阻塞式挂起
结构化并发

当我们了解了上述知识点以后,已经对协程有了一些基本的了解,但是对协程其他的的一些问题有点模糊,还是不明白如何使用协程,以及为什么会有协程等其他疑问,本文将继续探讨其他的协程问题

CoroutineContext

/**
 * Persistent context for the coroutine. It is an indexed set of [Element] instances.
 * An indexed set is a mix between a set and a map.
 * Every element in this set has a unique [Key].
 */
@SinceKotlin("1.3")
public interface CoroutineContext

协程上下文是协程中的重要概念,它管理了协程的生命周期,线程调度,异常处理等功能

CoroutineContext中的元素

我们 可以在每个协程块中访问CoroutineContext,它是Element的索引集,可以使用索引Key来访问上下文中的元素

Job
public interface Job : CoroutineContext.Element
协程的后台工作,可用来将协程取消
默认值: null,比如GlobalScope中的Job为null,

CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element
错误处理

ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element
负责线程的输入/输,Dispatcher继承于CoroutineDispatcher,我们通常使用Dispatcher去指定对应的协程执行线程

CoroutineName
协程的名称,一般用于调试

val job = GlobalScope.launch {
    coroutineContext[Job]
    coroutineContext[CoroutineExceptionHandler]
    coroutineContext[ContinuationInterceptor]
    coroutineContext[CoroutineName]
}

自定义CoroutineContext

你可以自定义一个Context当启动一个协程时,比如你需要自定义的CoroutineExceptionHandler以及执行线程等

//自定义CoroutineContext
val errorHandle = CoroutineExceptionHandler { context, error ->
    Log.e("Mike","coroutine error $error")
}

//指定自定义CoroutineContext
GlobalScope.launch(context = errorHandle) {
    delay(2000)
    throw Exception("test")
}
打印结果
coroutine error java.lang.Exception: test

Context也可以支持多个上下文元素的拼接,使用+/plus运算符

val job = Job()
val dispatcher = Dispatchers.IO
val errorHandle = CoroutineExceptionHandler { context, error ->
    Log.e("Mike","coroutine error $error")
}
//自定义CoroutineContext集
val context = job + dispatcher + errorHandle
GlobalScope.launch(context = context) {
    delay(2000)
    throw Exception("test")
}

上下文的切换
可以使用withContext,函数来改变协程的上下文,而仍然驻留在相同的协程中,当执行完withContext块之后又会返回到原始调度中

GlobalScope.launch {
    val res =  async {
        Log.e("Mike","get respone in ${Thread.currentThread().name}")
        delay(2000)
        "resposne data"
    }
    val response = res.await()
//执行Dispatchers.Main上下文
    withContext(Dispatchers.Main){
        Log.e("Mike","display respone $response in ${Thread.currentThread().name}")
    }
}
打印结果
get respone in DefaultDispatcher-worker-3
//两秒后
display respone resposne data in main

CoroutineContext的创建

  • 默认的CoroutineContext

我们之前可以直接通过CoroutineScopelaunchasync去创建一个协程,当我们直接使用

GlobalScope.launch {}

它会有一个默认的EmptyCoroutineContext

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
  • 手动传入的CoroutineContext
    我们可以自己构建一个CoroutineContext传入进行指定

  • 继承
    当我们在一个协程中创建了另一个子协程的时候,会发生什么,我们可以先看看launch函数的实现

GlobalScope.launch {
    launch {

    }
}

当子协程创建时调用launch函数,传入子Context或者默认

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
}

然后会调用newCoroutineContext(context),将子Context与父coroutineContext进行组装

public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = coroutineContext + context
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
        debug + Dispatchers.Default else debug
}

这是Context的plus规则,位于CoroutineContext中,也就是说子Context是由父Context和子构建的Context共同决定的,

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)
                }
            }
        }

加号操作符返回一个包含两个上下文所有 elements 的上下文集合,当两个上下文有 key 值相同的 element 时,它将丢弃掉操作符左侧上下文(父)中的 element,也就是子上下文优先,如果子上下文为默认的上下文EmptyCoroutineContext,则返回父上下文

GlobalScope.launch(Dispatchers.IO + errorHandling) {
    Log.e("Mike","------Parent context--->${coroutineContext[CoroutineExceptionHandler]}---${coroutineContext[ContinuationInterceptor]}")
    launch(Dispatchers.Main) {
        Log.e("Mike","------child context--->${coroutineContext[CoroutineExceptionHandler]}---${coroutineContext[ContinuationInterceptor]}")
    }
}
//打印结果
------Parent context--->errorHandling---IO
------child context--->errorHandling---Main

总结

  • CoroutineContext表示协程的上下文,包含此协程的相关信息,主要包含四个元素JobCoroutineExceptionHandlerContinuationInterceptorCoroutineName它们都实现了CoroutineContext.Element接口
  • CoroutineContext可以自由组装使用运算符+/plus的方式进行拼接,并在启动协程的时候指定
  • 当在协程中刚创建一个子协程时,子协程的上下文则由指定的子协程作用域(或默认)与父上下文组合生成
  • 可以使用withContext切换协程所执行的上下文,执行完毕之后又会回到原始上下文中
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,188评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,464评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,562评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,893评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,917评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,708评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,430评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,342评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,801评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,976评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,115评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,804评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,458评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,008评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,135评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,365评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,055评论 2 355

推荐阅读更多精彩内容