Kotlin 协程新手指南 —— 结构化并发

结构化并发(Structured Concurrency)是 Kotlin 协程最核心的设计哲学。理解了它,你就能真正驾驭协程的生命周期,避免资源泄漏和任务丢失。

一、为什么需要结构化并发?

在没有结构化并发的时代(比如直接用 ExecutorService 或原始线程),我们经常遇到两个问题:

  1. 任务泄漏:启动了一个后台任务,但忘记关闭它,导致资源浪费甚至崩溃。
  2. 父任务无法感知子任务:父任务完成了,但子任务还在后台乱跑,产生了不可预期的行为。
java 复制代码
// 传统线程池的问题
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
    // 长时间运行的任务
});
// 如果忘记 shutdown,应用关闭时这个任务还会在后台跑

结构化并发 的核心思想就是:让并发任务像普通代码一样有明确的作用域和生命周期------协程有开始,也有结束,且子协程的生命周期被父协程所管理。

二、结构化并发的核心思想

结构化并发可以概括为三个基本原则:

  1. 生命周期绑定 :协程的生命周期被限定在某个作用域内(如 CoroutineScope)。作用域结束时,其内部所有协程都会被自动取消。
  2. 父子关系:每个协程都有父协程,父协程会等待所有子协程完成后再完成自己。
  3. 错误传播:子协程中的未捕获异常会向上传播给父协程,导致父协程(以及相关兄弟协程)被取消,保持原子性。

结构化并发让异步代码的阅读顺序和执行顺序保持一致,这是协程优于回调的核心所在。

三、结构化协程的层级关系

在协程中,Job 形成了父子层级:

kotlin 复制代码
val parentJob = scope.launch {          // 根协程
    println("Parent started")
    val childJob = launch {             // 子协程1
        delay(1000)
        println("Child 1 done")
    }
    launch {                            // 子协程2
        delay(500)
        println("Child 2 done")
    }
    println("Parent waiting children")
}
parentJob.join()  // 父协程会等待两个子协程都完成

输出

text 复制代码
Parent started
Parent waiting children
Child 2 done
Child 1 done

重要规则

  • 父协程不会自动"结束"自己的执行,直到所有子协程都完成。
  • 父协程被取消时,会递归取消所有子协程。
  • 子协程抛出异常,默认会向上传播,导致父协程和兄弟协程被取消。

四、CoroutineScope:协程的作用域

CoroutineScope 是一个接口,它持有 CoroutineContext(主要是 Job),用于启动新协程。你可以把它理解成一个协程容器,管理着所有在这个作用域内启动的协程的生命周期。

kotlin 复制代码
class MyViewModel : ViewModel() {
    // viewModelScope 是 Android 提供的 CoroutineScope
    // 当 ViewModel 被清除时,这个作用域会被自动取消
    fun loadData() {
        viewModelScope.launch {    // 协程在 viewModelScope 内启动
            val data = fetchData()
            updateUI(data)
        }
    }
}

CoroutineScope 本质上只是一个持有 CoroutineContext 的对象。它的作用是:

  • 提供启动协程的"容器"
  • 通过其内部的 Job 管理协程层级
  • 提供取消时的统一入口

创建自定义作用域

kotlin 复制代码
val customScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + CoroutineName("MyScope"))

customScope.launch {
    // 这个协程的生命周期由 customScope 管理
}

// 需要清理时,取消整个作用域
customScope.cancel()

最佳实践 :在类中持有 CoroutineScope,并在类销毁时取消它(如 onClearedclose 等)。

五、coroutineScope vs supervisorScope

5.1 coroutineScope

coroutineScope 是一个挂起函数 ,它创建一个新的子作用域 ,并等待作用域内所有子协程完成,然后才返回。如果一个子协程失败(抛出异常),其他子协程会被取消,并且异常会重新抛出给调用者

kotlin 复制代码
suspend fun doWork() = coroutineScope {
    val job1 = launch { delay(1000); println("Job1 done") }
    val job2 = launch { delay(500); throw RuntimeException("Oops") }
    // job2 失败 -> job1 被取消,异常抛出给 doWork 的调用者
}

coroutineScope 的行为很像 runBlocking 的挂起版本 ------ 它保证作用域内所有任务完成后才继续,但不阻塞线程

5.2 supervisorScope

supervisorScopecoroutineScope 很像,但关键区别在于异常处理 :子协程的失败不会影响其他子协程,也不会立即抛出异常,而是像 async 一样,只在调用 await() 时才会抛出。

kotlin 复制代码
suspend fun doWorkWithSupervisor() = supervisorScope {
    val job1 = launch { delay(1000); println("Job1 done") }
    val job2 = launch { delay(500); throw RuntimeException("Oops") }
    // job2 失败了,但 job1 依然会完成
    // 异常不会在这里抛出,除非我们显式 await 或 join 到失败的协程
}

// 调用处可以捕获异常
runBlocking {
    try {
        doWorkWithSupervisor()
    } catch(e: Exception) {
        // 只有在 supervisorScope 内的子协程被 join/await 时,异常才会抛出
    }
}

5.3 对比表格

特性 coroutineScope supervisorScope
子协程失败影响其他 ✅ 会,兄弟协程被取消 ❌ 不会,兄弟协程继续执行
异常传播 立即抛出,向上传播 延迟抛出(只有 await/join 到失败子协程时)
典型用途 原子性操作:要么全成功,要么全失败 独立子任务:一个失败不影响其他
类似 类似 runBlocking 的挂起版 类似 SupervisorJob 的效果

六、Job vs SupervisorJob

6.1 Job

  • 普通协程的默认 Job
  • 子协程失败会导致父 Job 失败,进而取消所有其他子协程。
  • 父 Job 取消会递归取消所有子 Job。

6.2 SupervisorJob

  • SupervisorJob 是一种特殊的 Job,它不会因为子协程的失败而自动取消。其他子协程可以继续运行。
  • 常用于需要保持并行任务的独立性(比如 UI 界面中一个组件加载失败不影响其他组件)。
kotlin 复制代码
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

scope.launch {
    // 子协程 A
    delay(1000)
    throw RuntimeException("A failed")
}
scope.launch {
    // 子协程 B ------ 不会因为 A 失败而被取消
    delay(2000)
    println("B still works")
}

重要区别SupervisorJob 只影响父对子 的异常处理方向,不影响子对父------如果父 Job 被取消,所有子 Job 仍然会被取消。

SupervisorJob 的特性:

  • 子协程失败 → 只取消失败的那个协程,不影响兄弟
  • 失败不会向上取消父协程(父 scope 不会被取消)
  • ⚠️ 但"不向上传播"不等于异常被吞了SupervisorJob 的直接子协程在异常处理上被当作根协程 ------异常会交给 context 里的 CoroutineExceptionHandler,没有 handler 就走默认未捕获处理器,直接让 App 崩溃

6.3 在不同场景中使用

场景 推荐使用的 Job
一组任务必须原子完成(全成功或全失败) 普通 Job(默认)
独立的后台任务,一个失败不应影响其他 SupervisorJob
作用域内混合使用 coroutineScope / supervisorScope 根据需要动态创建子作用域

6.4 supervisorScope + Job 的常见误用

kotlin 复制代码
// ❌ 错误:作用域是 SupervisorJob,但使用 coroutineScope 时仍会取消兄弟?
val scope = CoroutineScope(SupervisorJob())
scope.launch {
    coroutineScope {   // 这个子作用域内仍使用普通 Job 语义!
        launch { throw RuntimeException() }
        launch { delay(1000); println("This may not run") }
    }
}
// coroutineScope 内部会覆盖外部的 SupervisorJob 语义,仍然会取消兄弟。

七、实战:结构化并发的最佳实践

7.1 使用 viewModelScopelifecycleScope

在 Android 开发中,直接用官方提供的作用域,避免手动管理:

kotlin 复制代码
class MyFragment : Fragment() {
    fun fetchData() {
        viewLifecycleOwner.lifecycleScope.launch {
            // 当 Fragment 的视图销毁时,这个协程会被自动取消
            val data = withContext(Dispatchers.IO) { repo.getData() }
            updateUI(data)
        }
    }
}

7.2 使用 coroutineScope 实现原子并行任务

kotlin 复制代码
suspend fun fetchThreeResources(): Triple<A, B, C> = coroutineScope {
    val a = async { fetchA() }
    val b = async { fetchB() }
    val c = async { fetchC() }
    Triple(a.await(), b.await(), c.await())
    // 如果任何一个 async 失败,整个函数失败,其他正在进行的请求会被自动取消
}

7.3 使用 supervisorScope 实现独立加载

kotlin 复制代码
suspend fun loadWidgets(): List<Widget> = supervisorScope {
    val jobs = widgetConfigs.map { config ->
        async { loadWidget(config) }
    }
    // 只返回成功加载的,失败的忽略
    jobs.mapNotNull { it.getOrNull() }
}

7.4 自定义作用域的生命周期管理

kotlin 复制代码
class MyService : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.IO) {
    fun start() {
        launch { doPeriodicWork() }
    }
    
    fun shutdown() {
        cancel()  // 取消整个作用域,所有子协程自动取消
    }
    
    private suspend fun doPeriodicWork() {
        while (isActive) {
            delay(5000)
            println("Working...")
        }
    }
}

八、总结速查表

概念 一句话总结
结构化并发 协程有明确的作用域和生命周期,父协程管理子协程,防止任务泄漏
父子关系 父协程等待所有子协程完成;父取消则递归取消子
CoroutineScope 协程的容器,持有上下文,用于启动协程,可统一取消
coroutineScope 挂起函数,创建子作用域,一个失败全部取消,并向上传播异常
supervisorScope 挂起函数,创建子作用域,一个失败不影响其他,异常延迟抛出
Job 普通 Job,子失败导致父失败
SupervisorJob 特殊 Job,子失败不影响父(但父取消仍会取消子)
原子性并行任务 使用 coroutineScope + async
独立并行任务(忽略失败) 使用 supervisorScope + async + getOrNull()

结构化并发是 Kotlin 协程区别于其他异步框架的最大优势。掌握它,你将写出更安全、更易读、更健壮的并发代码。

相关推荐
不会写DN1 小时前
通过php 中的Route:: 的写法了解什么是静态类调用
android·java·php
我命由我123451 小时前
由 ImageView 获取到的 Drawable 对象,它的 intrinsicWidth、intrinsicWidth 与实际图片的尺寸
java·开发语言·java-ee·android studio·android jetpack·android-studio·android runtime
BreezeDove2 小时前
【Android】AndroidStudio+Flutter开发建议环境变量
android·flutter
UXbot2 小时前
移动端UI设计工具选型指南:iOS与Android设计标准支持对比
android·前端·低代码·ios·交互·团队开发·ui设计
Kapaseker2 小时前
为什么 Java 要废弃 Thread.stop()?看完这篇你就懂了
android·kotlin
苦瓜花2 小时前
【Android】三大动画的实践
android
Mars-xq2 小时前
VSCode 开发 Android 时,类、方法无法跳转
android·ide·vscode
2601_961766643 小时前
【分享】Resprite安卓版|专业像素绘画,游戏美术创作工具
android·游戏美术
Mars-xq3 小时前
VSCode 开发Android 新手必装插件清单
android·ide·vscode