协程作用域 —— 结构化并发

给协程找一个「爹」——谁生谁管、谁死谁死,父子连锁永不泄漏

CoroutineScope Structured Concurrency coroutineScope supervisorScope viewModelScope

目录导航(点击跳转)

一、先看没有作用域会怎样

1到处乱 launch:没爹没妈的孩子
// 没有作用域:到处乱 launch
fun onUserLogin(userId: String) {
    GlobalScope.launch {           // ① 启动一个协程
        val data = fetchUser(userId)
        cacheUser(data)
    }

    GlobalScope.launch {           // ② 又启动一个
        val ads = fetchAds(userId)
        showAds(ads)
    }

    // ③ 再来一个
    GlobalScope.launch {
        updateLastLogin(userId)
    }
}
2三个灾难场景

场景1:用户快速退出登录onUserLogin返回了 → 但 3 个协程还在跑!→ 用户都走了,你还在更新他的缓存?浪费!

场景2:① 抛异常了→ ② 和 ③ 完全不知道 → ② 展示的广告基于旧数据,不一致

场景3:页面退出了→ 协程还在后台跑,没人管它们死活 → 内存泄漏

这就是没爹没妈的孩子——没人管,死活无人问。

二、作用域干了什么:给协程找一个「爹」

1有作用域的代码
fun onUserLogin(userId: String) = coroutineScope {  // ← 创建一个作用域
    launch { fetchUser(userId) }       // 子协程 1
    launch { fetchAds(userId) }        // 子协程 2
    launch { updateLastLogin(userId) } // 子协程 3
}
// ← 所有子协程都结束了,这行才执行
2父子关系图
onUserLogin coroutineScope 子协程们
onUserLogin 启动
进入 coroutineScope,创建三个子协程
1 子协程1: fetchUser ────→ 完成
2 子协程2: fetchAds ─────→ 完成
3 子协程3: updateLogin ──→ 完成
所有子协程结束,作用域才关闭
onUserLogin 返回

三、铁律一:父协程等所有子协程

1代码示例
coroutineScope {
    launch {
        delay(1000)
        println("A 完成")
    }
    launch {
        delay(2000)
        println("B 完成")
    }
    println("子协程都结束了我才打印这行")
}
// 输出:
// A 完成
// B 完成
// 子协程都结束了我才打印这行

父协程在 coroutineScope结束前会挂起,等所有子协程跑完。

2时间线
父协程: ├─ launch A ──────────→ 完成
├─ launch B ──────────────────→ 完成
└─ 在这挂起等着──────────────────────→ 两个都完成,继续走

四、铁律二:子协程挂,全家挂

1代码示例
coroutineScope {
    launch {
        delay(500)
        throw RuntimeException("我挂了!")      // A 出异常
    }
    launch {
        delay(1000)
        println("这行不会打印")                  // B 被取消
    }
}
// A 抛异常 → 父协程取消 → B 被取消 → 异常抛给调用者
2异常传播链
子A 父作用域 子B
子A: delay(500) → 抛异常!
父作用域收到异常 → 取消所有其他子协程
子B: 被取消(还没跑完就没了)
异常继续往上层调用者抛

五、铁律三:父挂,全家挂

1代码示例
coroutineScope {
    launch {
        delay(500)
        println("A 跑完了")
    }

    delay(100)          // 等 100ms
    throw RuntimeException("爹挂了")  // 在子协程跑完前爹就挂了
}
// A 还没跑完就被取消
2有爹 vs 没爹对比
没有作用域(GlobalScope)
GlobalScope.launch { 网络请求 }
GlobalScope.launch { 写缓存 }
GlobalScope.launch { 更新UI }
野孩子:各管各、互不知情、没人管死活
页面退出 → 协程还在跑(泄漏!)
有了作用域(coroutineScope)
coroutineScope {
  launch { 网络请求 }
  launch { 写缓存 }
  launch { 更新UI }
}
作用域结束 → 所有子协程完成
scope.cancel() → 所有子协程取消 → 干净利落
3三条铁律总览图
铁律触发条件后果
铁律一父协程进入 coroutineScope父协程挂起,等所有子协程完成才继续
铁律二任意子协程抛未捕获异常父取消 → 所有兄弟姐妹被取消 → 异常向上传播
铁律三父协程被取消或抛异常所有子协程被取消 → 一个不留

六、两个最常用的作用域构建器

1coroutineScope —— 一个都不能少
suspend fun loadData() = coroutineScope {
    val user = async { fetchUser() }      // 并发
    val ads  = async { fetchAds() }       // 并发
    UserPage(user.await(), ads.await())    // 两个都拿到才返回
}
2supervisorScope —— 一个挂了不影响其他
suspend fun loadSafe() = supervisorScope {
    launch { fetchUser() }     // 挂了
    launch { fetchAds() }      // 不受影响,继续跑
    launch { updateUI() }      // 不受影响
}
3两种作用域对比
coroutineScope
子1:
子2: 挂 → 子3 被取消
子3: 被取消
整个作用域失败
supervisorScope
子1: 挂了
子2: 不受影响
子3: 不受影响
部分失败,不影响其他

七、Android 里最典型的用法

1viewModelScope:生命周期 = 协程生命周期
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {        // ViewModel 销毁时自动取消
            val data = repository.fetch()
            _state.value = data
        }
    }
}
2页面生命周期链
Activity ViewModel viewModelScope
1 Activity 创建 → ViewModel 创建 → viewModelScope 开工
2 launch { 请求数据... }
用户在浏览页面...
3 Activity 销毁 → ViewModel 销毁
viewModelScope.cancel()
请求没完成?取消!回调不会执行,没有泄漏

八、核心思想总结

第五章核心要点

一句话总结第五章

无作用域:漫天野孩子,挂了跑了没人管,泄漏、异常、数据不一致。有作用域:每个协程都有爹,父子连锁——爹等孩子、孩子死全家死、爹死全家死、生命周期自动绑定。这就叫「结构化并发」= Structured Concurrency。