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

相关推荐
我命由我123454 小时前
Android 开发问题:getLeft、getRight、getTop、getBottom 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Modu_MrLiu4 小时前
Android实战进阶 - 用户闲置超时自动退出登录功能详解
android·超时保护·实战进阶·长时间未操作超时保护·闲置超时
Jeled4 小时前
Android 网络层最佳实践:Retrofit + OkHttp 封装与实战
android·okhttp·kotlin·android studio·retrofit
信田君95274 小时前
瑞莎星瑞(Radxa Orion O6) 基于 Android OS 使用 NPU的图片模糊查找APP 开发
android·人工智能·深度学习·神经网络
tangweiguo030519875 小时前
Kotlin 实现 Android 网络状态检测工具类
android·网络·kotlin
nvvas6 小时前
Android Studio JAVA开发按钮跳转功能
android·java·android studio
怪兽20146 小时前
Android多进程通信机制
android·面试
叶羽西7 小时前
Android CarService调试操作
android
千里马-horse7 小时前
在android中 spdlog库的log如何在控制台上输出
android·c++·spdlog