协程的父子关系

协程的父子关系是 Kotlin 协程里非常重要的概念,核心叫:

结构化并发 Structured Concurrency

可以简单理解为:

一个 CoroutineScope 里面启动的协程,会归这个 Scope 管。
一个协程里面再启动新的协程,新的协程默认就是它的子协程。
父协程会等待子协程结束;父协程取消,子协程也会取消。


1. 最简单的父子关系

看这个例子:

viewModelScope.launch {
    launch {
        delay(1000)
        println("子协程 1 完成")
    }

    launch {
        delay(2000)
        println("子协程 2 完成")
    }

    println("父协程代码执行到这里")
}

结构大概是:

viewModelScope
└── 父协程 launch
    ├── 子协程 1 launch
    └── 子协程 2 launch

注意一点:

println("父协程代码执行到这里")

可能会很快打印,但父协程不会真正结束,它会等待两个子协程都执行完。


2. 父协程会等待子协程

例子:

viewModelScope.launch {
    launch {
        delay(2000)
        println("子协程完成")
    }

    println("父协程代码结束")
}

输出类似:

父协程代码结束
子协程完成

虽然父协程里的代码先执行完了,但父协程的 Job 还没有真正完成,因为它要等子协程完成。

你可以理解成:

父协程 = 自己的代码 + 所有子协程

只有这些全部结束,父协程才算结束。


3. 父协程取消,子协程也取消

比如:

val job = viewModelScope.launch {
    launch {
        repeat(10) {
            delay(500)
            println("子协程执行中 $it")
        }
    }
}

delay(1200)
job.cancel()

当你取消父协程:

job.cancel()

里面的子协程也会被取消。

结构:

父协程 cancel
    ↓
所有子协程一起 cancel

这就是为什么 Android 里:

viewModelScope.launch { }

当 ViewModel 销毁时,里面的任务会自动取消。

因为:

ViewModel cleared
    ↓
viewModelScope 被取消
    ↓
viewModelScope 下所有协程全部取消

4. 子协程异常,默认会影响父协程

默认情况下:

一个普通子协程抛异常,会导致父协程取消,然后父协程会取消其它子协程。

例如:

viewModelScope.launch {
    launch {
        delay(500)
        throw RuntimeException("子协程 1 出错了")
    }

    launch {
        repeat(10) {
            delay(300)
            println("子协程 2 执行中 $it")
        }
    }
}

当子协程 1 抛异常:

子协程 1 异常
    ↓
父协程被取消
    ↓
子协程 2 也被取消

这就是普通协程的默认行为。


5. SupervisorJob:子协程之间互不影响

有时候你希望:

一个子任务失败,不要影响其它子任务。

这时可以用 supervisorScopeSupervisorJob

推荐方式:supervisorScope

viewModelScope.launch {
    supervisorScope {
        launch {
            delay(500)
            throw RuntimeException("任务 1 失败")
        }

        launch {
            repeat(5) {
                delay(300)
                println("任务 2 继续执行 $it")
            }
        }
    }
}

这时:

任务 1 失败
任务 2 不会被取消

结构变成:

viewModelScope
└── 父协程
    └── supervisorScope
        ├── 任务 1 失败
        └── 任务 2 继续

6. 普通父子关系 vs Supervisor

普通 Job

父协程
├── 子协程 A 失败
└── 子协程 B 被取消

默认是:

一个孩子出事,全家取消

SupervisorJob / supervisorScope

父协程
├── 子协程 A 失败
└── 子协程 B 继续

可以理解为:

一个孩子出事,不影响其他孩子

7. launch 和 async 的父子关系

launchasync 都会创建子协程。

launch

viewModelScope.launch {
    val job = launch {
        delay(1000)
        println("launch 子协程")
    }
}

launch 返回的是:

Job

它主要用于“不需要返回结果”的任务。


async

viewModelScope.launch {
    val deferred = async {
        delay(1000)
        "用户数据"
    }

    val result = deferred.await()
    println(result)
}

async 返回的是:

Deferred<T>

它用于“需要返回结果”的任务。

结构:

父协程 launch
└── 子协程 async

8. async 的异常要注意

例如:

viewModelScope.launch {
    val deferred = async {
        throw RuntimeException("async 出错")
    }

    deferred.await()
}

async 的异常通常会在:

deferred.await()

这里暴露出来。

所以一般要这样处理:

viewModelScope.launch {
    try {
        val deferred = async {
            repository.getUser()
        }

        val user = deferred.await()
    } catch (e: Exception) {
        // 处理异常
    }
}

9. coroutineScope:创建一个新的子作用域

coroutineScope 会创建一个新的协程作用域,并且等待里面所有子协程完成。

viewModelScope.launch {
    coroutineScope {
        launch {
            delay(1000)
            println("任务 1")
        }

        launch {
            delay(2000)
            println("任务 2")
        }
    }

    println("两个任务都完成后,才执行这里")
}

输出:

任务 1
任务 2
两个任务都完成后,才执行这里

结构:

viewModelScope
└── launch
    └── coroutineScope
        ├── launch 任务1
        └── launch 任务2

coroutineScope 的特点:

会等待所有子协程完成
只要一个子协程失败,其它子协程会被取消

10. supervisorScope:失败隔离的子作用域

viewModelScope.launch {
    supervisorScope {
        launch {
            throw RuntimeException("任务 1 失败")
        }

        launch {
            delay(1000)
            println("任务 2 仍然执行")
        }
    }
}

supervisorScope 的特点:

会等待所有子协程完成
某个子协程失败,不会自动取消其它子协程

不过要注意,失败的子协程异常还是要处理,否则可能会打印崩溃日志。

建议:

viewModelScope.launch {
    supervisorScope {
        launch {
            try {
                repository.loadA()
            } catch (e: Exception) {
                // 单独处理任务 A 异常
            }
        }

        launch {
            try {
                repository.loadB()
            } catch (e: Exception) {
                // 单独处理任务 B 异常
            }
        }
    }
}

11. withContext 也是父子关系吗?

withContext 不像 launch 那样启动一个并发的新任务,它更像是:

切换上下文执行一段代码,并等待结果返回。

例如:

viewModelScope.launch {
    val user = withContext(Dispatchers.IO) {
        repository.getUser()
    }

    _user.value = user
}

结构上它仍然属于当前协程的一部分。

如果外层协程取消,withContext 里的代码也会取消。

viewModelScope
└── launch
    └── withContext(IO)

但它不是那种“并发跑多个子任务”的感觉,更像是当前协程切换到 IO 去执行一段。


12. Android 里的父子关系

viewModelScope

class UserViewModel : ViewModel() {

    fun loadUser() {
        viewModelScope.launch {
            val user = repository.getUser()
        }
    }
}

结构:

ViewModel
└── viewModelScope
    └── loadUser 的协程

当 ViewModel 销毁:

ViewModel.onCleared()
    ↓
viewModelScope 取消
    ↓
里面所有协程取消

lifecycleScope

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect {
            // 更新 UI
        }
    }
}

结构:

Activity / Fragment
└── lifecycleScope
    └── 收集 Flow 的协程

当 Activity/Fragment 销毁:

Lifecycle destroyed
    ↓
lifecycleScope 取消
    ↓
里面所有协程取消

13. 不推荐 GlobalScope

比如:

GlobalScope.launch {
    repository.getUser()
}

这个协程没有跟 Activity、Fragment、ViewModel 绑定。

也就是说:

页面销毁了,它可能还在跑
ViewModel 销毁了,它可能还在跑

容易导致:

  • 内存泄漏
  • 无效回调
  • 难以取消
  • 生命周期混乱

所以 Android 里一般用:

viewModelScope.launch { }

或者:

lifecycleScope.launch { }

而不是:

GlobalScope.launch { }

14. 常见代码对比

串行执行

viewModelScope.launch {
    val user = repository.getUser()
    val orders = repository.getOrders()
}

执行顺序:

先 getUser
再 getOrders

并行执行

viewModelScope.launch {
    val userDeferred = async {
        repository.getUser()
    }

    val ordersDeferred = async {
        repository.getOrders()
    }

    val user = userDeferred.await()
    val orders = ordersDeferred.await()
}

执行顺序:

getUser 和 getOrders 同时开始
await 等待两个结果

结构:

viewModelScope
└── launch 父协程
    ├── async 获取用户
    └── async 获取订单

15. 最重要的规则总结

你可以记这几条:

1. CoroutineScope 管理协程生命周期

2. 在一个协程里 launch/async 出来的协程,默认是它的子协程

3. 父协程取消,子协程全部取消

4. 普通 Job 下,子协程异常会取消父协程,进而取消其它兄弟协程

5. coroutineScope:一个失败,整体失败

6. supervisorScope / SupervisorJob:一个失败,不影响其它子协程

7. viewModelScope 跟 ViewModel 生命周期绑定

8. lifecycleScope 跟 Activity/Fragment 生命周期绑定

9. 不推荐随便用 GlobalScope

一句话理解

协程的父子关系就是:
任务要有归属,父任务负责管理子任务。父取消,子取消;普通情况下子失败,父也失败。

在 Android 里:

viewModelScope / lifecycleScope
就是最外层的“家长”
launch / async
就是这个家长下面创建出来的“孩子任务”
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇