Kotlin 协程的灵魂:结构化并发详解

为什么需要结构化

想象一个场景:应用发起了一个网络请求来获取数据,并将响应数据更新到 UI 上。如果用户在网络请求结束前关闭了页面,此时后台线程还活跃着,依旧在等待响应。并在得到响应后,尝试更新一个已经不存在的 UI 控件。

可以看到,这不仅会浪费资源,还可能会导致程序崩溃。

而协程通过结构化并发,能够将并发任务的管理变得容易。

CoroutineScope 与 Job:控制协程的生命周期

结构化并发的基础在于 CoroutineScopeJob

通过 launch 构建器启动协程后,会返回一个 Job 对象。我们可以把这个 Job 对象看作是一个协程,因为我们可以通过它来控制协程任务,比如取消协程(Job.cancel())、启动创建好的协程(Job.start())、查看协程的元数据。

只要在启动协程时,添加参数 start = CoroutineStart.LAZY,协程就不会自动启动。

如果我们需要在用户关闭界面时取消协程任务,只需这样:

kotlin 复制代码
private var job: Job? = null

// 启动协程
job = lifecycleScope.launch {
    // ...耗时操作...
}

// 在Activity或Fragment中
override fun onDestroy() {
    super.onDestroy()
    job?.cancel()
}

当然,对于 lifecycleScope 来说,没必要手动去取消,因为它会在 onDestroy 中自动取消其所有子协程。如果我们想要自由控制某个协程的生命周期,就需要持有协程的 Job 对象来手动取消。

我们也可以通过 CoroutineScope 协程作用域的 cancel 函数来取消协程,区别在于 CoroutineScope.cancel() 会取消由它启动的所有协程。

一般没必要手动创建 CoroutineScope,Android 已经提供好了 lifecycleScopeviewModelScope 这种与组件生命周期绑定的 CoroutineScope,当 Activity 或 Fragment 销毁时,会自动调用 cancel()

同时,我们也可以把 launchasync 代码块内部的 CoroutineScope 接收者(this)看作是协程对象。它是协程顶级的管理器,我们可以通过它来获取协程的所有功能和属性。

比如,我们可以在 launch 代码块中获取当前协程的协程调度器和 Job 对象。

kotlin 复制代码
lifecycleScope.launch {
    // 获取当前协程的 Job 对象
    val job = this.coroutineContext[Job]
    // 获取当前协程的调度器对象
    val dispatcher = this.coroutineContext[CoroutineDispatcher]
}

可以看出,CoroutineScopeJob 并非并列关系,而是从属关系。CoroutineScope 包含了所有的功能,而 Job 只负责一部分。

结构化取消

CoroutineScopeJob 结合,带来了级联取消(Cascading Cancellation) 机制。

在一个协程中启动的协程,会自动变为该协程的子协程,这得益于父协程 block 块的隐式接收者 CoroutineScope。我们在父协程内部调用的 launch,实际上调用的是 this.launch

这个 CoroutineScopethis) 对象,是父协程内部专属的对象,而非启动父协程的 CoroutineScope 对象。这个对象在启动子协程时,携带了父协程的 Job 实例以及协程的上下文信息,所以能够形成父子层级关系。

kotlin 复制代码
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    // 这个StandaloneCoroutine类型的coroutine对象,同时实现了Job和CoroutineScope接口
    // 是方法返回的 Job 对象,也是代码块的接收者(this)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine 
}

所以,下面的代码会自动建立父子关系。

kotlin 复制代码
lifecycleScope.launch { // 父协程
    launch { // 子协程 1
        delay(1000)
        Log.d("Coroutine", "子协程 1 完成")
    }

    launch { // 子协程 2
        delay(2000)
        Log.d("Coroutine", "子协程 2 完成")
    }
}

只要有一个 CoroutineScope 被取消,或是父 Job 被取消,那么取消的指令也会向下传给其内部启动的所有子协程、孙子协程...

但如果协程内部启动协程时,使用的 CoroutineScope 并不是当前协程的 this,就构不成父子关系。例如下面的代码中,outerJob 并非 innerJob 的父协程,innerJobouterJob 是兄弟关系,它们的父协程是 CoroutineScope 内部创建的 Job 实例。

kotlin 复制代码
val coroutineScope = CoroutineScope(Dispatchers.IO)
val outerJob = coroutineScope.launch {
    val innerJob = coroutineScope.launch {
        // ...
    }
}

结构化完成

一个父协程会等待其所有子协程完成后,自己才会进入完成状态。

kotlin 复制代码
lifecycleScope.launch {
    val parentJob = launch { // 父协程
        launch { // 子协程
            delay(2000)
            Log.d("Coroutine", "子协程完成")
        }
        Log.d("Coroutine", "父协程代码执行完毕")
    }

    parentJob.join() // 等待父协程及其所有子协程完成
    Log.d("Coroutine", "所有工作都已结束")
}

所以我们在执行复杂的任务时,只需对父 Job 调用 join,等到它返回时,就表示其内部所有的子任务都完成了。

并行任务的交互:async 和 await

如果我们要从并行的任务中获取结果,该怎么办?

比如同时请求用户信息和用户的好友列表,并将两者的结果合并显示到界面中。

这时,我们可以使用协程提供的构建器 CoroutineScope.async,它和 launch 类似,只不过它会返回一个 Deferred<T> 对象(意为"延迟的值"),我们可以通过这个对象获取任务的结果,具体是调用它的 await() 挂起函数。

kotlin 复制代码
lifecycleScope.launch {
    val userDeferred = async {
        delay(1000) // 模拟网络请求
        "userInfo"
    }
    val friendsDeferred = async {
        delay(1500) // 模拟网络请求
        "Friends"
    }

    // ...两个任务并行执行...

    // 挂起当前协程等待结果
    val user = userDeferred.await()
    val friends = friendsDeferred.await()
    
    val result = "user: $user, friends: $friends"
    println(result)
}

上述代码中 async 函数的调用都在父协程 launch 中,所以这两个任务都为父协程的子协程。只要父协程被取消,这两个并行的任务也会一起被取消。

如果某个任务的执行,需要等待另一个任务执行完毕,你也可以使用 Job.join()

kotlin 复制代码
lifecycleScope.launch {
    val initJob = launch {
        Log.d("Coroutine", "初始化任务开始...")
        delay(500) // 模拟初始化耗时
        Log.d("Coroutine", "初始化任务完成")
    }
    
    launch {
        // ...执行其他不依赖初始化的操作...
        delay(200)

        // 等待初始化完成
        initJob.join()

        // ...执行依赖初始化的操作...
        Log.d("Coroutine", "开始执行依赖初始化的工作")
    }
}

当然,通过 launch 启动的协程是获取不到结果的。

你也许会想到线程中的 join,不过线程的结构化管理比较复杂,并不常用。

相关推荐
消失的旧时光-194315 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
有位神秘人15 小时前
kotlin与Java中的单例模式总结
java·单例模式·kotlin
Jinkxs15 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&15 小时前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin
golang学习记15 小时前
IntelliJ IDEA 2025.3 重磅发布:K2 模式全面接管 Kotlin —— 告别 K1,性能飙升 40%!
java·kotlin·intellij-idea
LDORntKQH15 小时前
基于深度强化学习的混合动力汽车能量管理策略 1.利用DQN算法控制电池和发动机发电机组的功率分配 2
android
冬奇Lab15 小时前
Android 15 ServiceManager与Binder服务注册深度解析
android·源码·源码阅读
2501_9160088917 小时前
深入解析iOS机审4.3原理与混淆实战方法
android·java·开发语言·ios·小程序·uni-app·iphone
独行soc19 小时前
2026年渗透测试面试题总结-20(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
常利兵19 小时前
2026年,Android开发已死?不,它正迎来黄金时代!
android