为了账号安全,请及时绑定邮箱和手机立即绑定

什么是Kotlin协程的CoroutineContext及其内部工作机制?

标签:
Kotlin

大家好,

今天,我们将再次探讨 Kotlin 协程,这可是一个很重要的主题。要想写出完美的代码,需要好好理解 Kotlin 协程的每一个方面;否则,那就像是一种权宜之计。

我们已经多次用到CoroutineContext,并且在很多地方都见过它。咱们详细聊聊。

  • 协程范围会用到协程上下文。

  • ViewModelScope, 使用 CoroutineContext。

任务的协程上下文。

  • 如果你记得我们之前写过的关于协程内部的文章,那么你就会明白,每个挂起函数的参数中都有一个 Continuation,并且它们都会使用 CoroutineContext。

因此,我认为我们有必要搞清楚 CoroutineContext 到底是个什么东西。协程中有许多地方都用到了 CoroutineContext。

CoroutineContext 接口表示一个元素或一组元素的集合,概念上类似于映射或集合的数据结构。它是一个带有索引的 Element 实例集合,例如 JobCoroutineNameCoroutineDispatcher 等。每个 CoroutineContext.Element 本身也是一个 CoroutineContext,允许将多个元素组合起来。这种结构表明 CoroutineContext 实现了组合设计模式。

我们先看一些代码,然后再看看它的内部是如何工作的。

fun main() {  
    // 创建一个名为 "A name" 的 CoroutineName 实例
    val name: CoroutineName = CoroutineName("A name")  
    // 将 CoroutineName 实例作为 CoroutineContext.Element
    val element: CoroutineContext.Element = name  
    // 使用 element 创建一个 CoroutineContext
    val context: CoroutineContext = element  

    // 创建一个 Job 实例
    val job: Job = Job()  
    // 将 Job 实例作为 CoroutineContext.Element
    val jobElement: CoroutineContext.Element = job  
    // 使用 jobElement 创建一个 CoroutineContext
    val jobContext: CoroutineContext = jobElement  

    // 使用 name 和 job 实例创建一个新的 CoroutineContext 实例 ctx
    val ctx: CoroutineContext = name + job  
}

CoroutineContext 设计得易于使用。你可以直接传递单个元素(如 JobCoroutineDispatcher),而无需将它放入集合,因为单个元素本身就已经被视为一个 CoroutineContext

如果你需要同时使用多个元素,你只需将它们组合起来。生成的 CoroutineContext 将会包含你组合的所有元素。这使得创建和管理 CoroutineContext 更加简单又灵活。这让你在使用 CoroutineContext 时更加简单灵活。

我们来看看内部的代码,看看它们是怎么加进去的。

解释一下:

  • 因为 CoroutineContext 表示一个集合,你可以使用 get 方法通过特定的键来获取元素。你也可以用方括号,因为在 Kotlin 中 get 是一个操作符,可以这样调用。结果是一个与键关联类型的可空元素。这类似于从映射中获取元素:如果元素在上下文中存在,将返回该元素;如果不存在,则返回 null
  • 同样地,对于加号,你可以选择用“+”或.plus,因为加号在这里也是一个操作符。
  • 如果你查看代码会发现,如果传入 EmptyCoroutineContext,将返回原值,这意味着它不会做任何改动。
  • 还有一个函数叫 fold,它被加号操作符在添加新上下文时使用。

折叠:

  • 目的:fold 函数将 CoroutineContext 中的所有元素结合成一个单一的结果。
  • 它有两个参数:initialoperationinitial 是累积的初始值,而 operation 是一个函数,它接收当前的累积值和一个元素作为输入,然后返回一个新的累积值。
  • 过程:函数从 initial 值开始。然后将 operation 应用于这个初始值和 CoroutineContext 中的每个元素,依次从左到右。每次的应用结果都会成为下一个步骤的新累积值。

在协程上下文中查找组件

我们先来看一个例子吧。

fun main() {  
    val ctx: 协程上下文(CoroutineContext) = 协程名称(CoroutineName)("一个名称")  

    val 协程名称: 协程名称(CoroutineName)? = ctx[CoroutineName]  
    // 可以也可以是 ctx.get(CoroutineName)  
    println(协程名称?.名称(name))   
    val 作业: 作业(Job)? = ctx[Job]  
    println(作业) // null  
}  

输出:  
一个名称  
null

解释:

  • 要找到一个 CoroutineName,你只需要使用 CoroutineName。这是因为 CoroutineName 在 Kotlin 中引用其伴生对象,从而使从上下文中获取 CoroutineName 更为方便。
  • 我们来看看 CoroutineName 的实现细节。

在 kotlinx.coroutines 库中,通常使用伴生对象作为同名元素的键,这样更容易记住这些名称。

增加一些上下文:

真正让CoroutineContext变得非常有用的是它能够合并两个上下文。当添加两个具有不同键的元素时,生成的上下文可以同时响应这两个键。就像在映射中,如果你添加了一个与现有元素具有相同键的新元素,那么新元素会替换旧元素一样。

示例 1:  
fun main() {  
    val ctx1: CoroutineContext = CoroutineName("Name1")  
    println(ctx1[CoroutineName]?.name) // Name1  
    println(ctx1[Job]?.isActive) // null  

    val ctx2: CoroutineContext = Job()  
    println(ctx2[CoroutineName]?.name) // null  
    println(ctx2[Job]?.isActive) // true,因为这是默认的活动状态  

    val ctx3 = ctx1 + ctx2  
    println(ctx3[CoroutineName]?.name) // Name1  
    println(ctx3[Job]?.isActive) // true  
}  

示例 2:  
fun main() {  
    val ctx1: CoroutineContext = CoroutineName("Name1")  
    println(ctx1[CoroutineName]?.name) // Name1  

    val ctx2: CoroutineContext = CoroutineName("Name2")  
    println(ctx2[CoroutineName]?.name) // Name2  

    val ctx3 = ctx1 + ctx2  
    println(ctx3[CoroutineName]?.name) // Name2  
}

当你使用加号来添加一个新的CoroutineName、Job或Context时,你也可以通过调用minusKey函数来移除。

minus 运算符对 CoroutineContext 没有进行重载。我认为这是因为它含义不够明确,正如《Effective Kotlin》中的第 11 项所述,运算符的用法应与其函数名含义一致。

fun main() {  
    val ctx = 协程名称("Name1") + Job()  
    println(ctx[协程名称]?.名称) // Name1  
    println(ctx[Job]?.isActive) // true  

    val ctx2 = ctx.minusKey(协程名称)  
    println(ctx2[协程名称]?.名称) // null  
    println(ctx2[Job]?.isActive) // true  

    val ctx3 = (ctx + 协程名称("Name2"))  
        .minusKey(协程名称)  
    println(ctx3[协程名称]?.名称) // null  
    println(ctx3[Job]?.isActive) // true  
}

注释:

  • CoroutineName 被翻译为 协程名称
  • name 被翻译为 名称
  • 其余未翻译的部分如 JobisActive 保留英文,因为它们是技术术语,在中文中通常直接使用英文。
  • 保留了原始的代码注释,将它们翻译为中文以增强理解。

协程的环境与构建工具:

  • 协程上下文实际上是一种用来持有和传递数据的方法。这种传递方式体现了父子协程之间的关系。我们可以说子协程继承了父协程的上下文。
  • 然而,每个子协程还可以在参数中定义自己特定的上下文。这个特定的上下文会覆盖继承来的上下文。
    fun CoroutineScope.log(msg: String) {  
        val name = coroutineContext[CoroutineName]?.name  
        println("[$name] $msg")  
    }  

    fun main() = runBlocking(CoroutineName("main")) {  
        log("开始运行") // [main] 开始运行  
        val v1 = async {  
            delay(500)  
            log("异步运行") // [main] 异步运行  
            42  
        }  
        launch(CoroutineName("Launch")) { // 指定的 CoroutineName 有其特定的 CoroutineContext  
            delay(1000)  
            log("启动 launch") // [Launch] 启动 launch  
        }  
        log("答案是 ${v1.await()},即延迟后返回的值")  
        // [main] 答案是 42  
    }

自己创建我们的情境,

  • 虽然不常需要自定义协程上下文,我们可以轻松地创建自己的协程上下文。最简单的方法是创建一个实现CoroutineContext.Element接口的类。这个类需要一个类型为CoroutineContext.Key<*>的属性,称为key,用于标识该上下文。通常的做法是将该类的伴生对象作为key。以下是一个简单的协程上下文实现的示例。
    import kotlinx.coroutines.launch  
    import kotlinx.coroutines.withContext  
    import kotlin.coroutines.CoroutineContext  
    import kotlin.coroutines.coroutineContext  

    class CounterContext(  
        private val name: String  
    ) : CoroutineContext.Element {  
        override val key: CoroutineContext.Key<*> = Key  
        private var nextNumber = 0  

        fun printNext() {  
            println("$name: $nextNumber")  
            nextNumber++  
        }  

        companion object Key : CoroutineContext.Key<CounterContext>  
    }  

    suspend fun printNext() {  
        coroutineContext[CounterContext]?.printNext()  
    }  

    suspend fun main(): Unit =  
        withContext(CounterContext("外部计数器")) {  
            printNext() // 启动外部计数器: 0  
            launch {  
                printNext() // 启动外部计数器: 1  
                launch(CounterContext("内部计数器")) {  
                    printNext() // 启动内部计数器: 0  
                    printNext() // 启动内部计数器: 1  
                    launch {  
                        printNext() // 启动内部计数器: 2  
                    }  
                }  
            }  
            printNext() // 启动外部计数器: 2  
        }

好的,这节课就到这里吧。

你们玩得怎么样?

尝试一下,如果有任何问题,可以在评论区提问。

在这里关注我,看看有趣的话题

还有 LinkedIn 上可以关注我,一起扩展我们的开发者圈子吧

很快就会和大家分享一些新的超赞内容。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消