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,不过线程的结构化管理比较复杂,并不常用。

相关推荐
Meteors.38 分钟前
安卓进阶——OpenGL ES
android
椰羊sqrt2 小时前
CVE-2025-4334 深度分析:WordPress wp-registration 插件权限提升漏洞
android·开发语言·okhttp·网络安全
2501_916008892 小时前
金融类 App 加密加固方法,多工具组合的工程化实践(金融级别/IPA 加固/无源码落地/Ipa Guard + 流水线)
android·ios·金融·小程序·uni-app·iphone·webview
sun0077003 小时前
Android设备推送traceroute命令
android
来来走走3 小时前
Android开发(Kotlin) 高阶函数、内联函数
android·开发语言·kotlin
2501_915921433 小时前
Fastlane 结合 开心上架(Appuploader)命令行版本实现跨平台上传发布 iOS App 免 Mac 自动化上架实战全解析
android·macos·ios·小程序·uni-app·自动化·iphone
雨白4 小时前
重识 Java IO、NIO 与 OkIO
android·java
啦啦9117145 小时前
Niagara Launcher 全新Android桌面启动器!给手机换个门面!
android·智能手机
游戏开发爱好者85 小时前
iOS 上架要求全解析,App Store 审核标准、开发者准备事项与开心上架(Appuploader)跨平台免 Mac 实战指南
android·macos·ios·小程序·uni-app·iphone·webview
xrkhy5 小时前
canal1.1.8+mysql8.0+jdk17+redis的使用
android·redis·adb