协程(入门)
Kotlin 协程系统指南(从入门到高级实战)
目标读者:Android/Kotlin 开发者
阅读目标:不仅会“用 API”,还要理解协程的设计思想、边界和工程落地方式。
目录
- 协程为什么出现:先解决了什么问题
- 协程核心概念全景图
- 第一段协程代码(逐行讲解)
- suspend 的本质:挂起不是阻塞
- 作用域与生命周期:结构化并发的入口
- Dispatcher 与线程切换策略
- 并发组合:launch/async/await 正确使用
- 取消与超时:可控结束比成功更重要
- 异常传播与 Supervisor 思维模型
- Flow 入门:什么是冷流(Cold Flow)
- 冷流 vs 热流:StateFlow/SharedFlow 全面对比
- Flow 常见操作符与背压处理
- Android 架构落地:ViewModel + Repository
- Channel 与 Actor:协程间通信与串行化状态
- 高级主题:并发安全、性能优化、调试手段
- 协程测试:runTest、虚拟时间与可测性
- 完整业务案例:审批页并发加载 + 重试 + 超时 + 事件流
- 常见误区与实践清单(可直接贴团队规范)
1. 协程为什么出现:先解决了什么问题
1.1 传统线程模型的痛点
- 线程创建和上下文切换成本高
- 回调嵌套(callback hell)使代码难维护
- 异步任务生命周期难统一管理(容易泄漏)
- 错误处理分散(try-catch 到处写)
1.2 协程提供的价值
- 用“同步代码风格”表达异步逻辑
- 轻量级并发(可创建大量协程)
- 结构化并发保证任务生命周期可控
- 配合 Flow/StateFlow 可自然描述 UI 数据流
1.3 一个直观类比
把“线程”想成“整间会议室”,创建和切换都昂贵;
把“协程”想成“会议中的议程项”,挂起时让出时间片,成本很低。
2. 协程核心概念全景图
- Coroutine:可挂起可恢复的任务单元
- suspend:标记可挂起函数
- CoroutineScope:协程的生命周期容器
- Job:协程句柄(取消、状态跟踪)
- Dispatcher:运行调度器(Main/IO/Default)
- CoroutineContext:上下文集合(Job + Dispatcher + Name + Handler)
可以先记住一句话:
协程 = 任务(suspend) + 生命周期(Scope/Job) + 执行策略(Dispatcher)
3. 第一段协程代码(逐行讲解)
import kotlinx.coroutines.*
fun main() = runBlocking {
println("main start: ${Thread.currentThread().name}")
val job = launch {
println("child launch")
delay(500)
println("child done")
}
println("main waiting")
job.join()
println("main end")
}
说明:
runBlocking:桥接普通函数与协程世界,阻塞当前线程直到内部完成(测试/示例常用)launch:启动无返回值协程delay:挂起,不阻塞线程join:等待job完成
4. suspend 的本质:挂起不是阻塞
4.1 什么是“挂起”
挂起是:当前协程暂时停下,把线程让给别人,等条件满足再恢复。
4.2 生活类比
你在餐厅点餐后不会一直站在柜台前堵住别人(阻塞),而是拿号去等(挂起),叫号再回来继续。
4.3 代码示例
suspend fun fetchUser(): String {
delay(300)
return "Tom"
}
suspend fun loadPage() {
val user = fetchUser()
println("user=$user")
}
4.4 常见误区
suspend不是“自动异步线程切换”suspend函数内部如果做 CPU 重活且不切到Default,依旧可能卡主线程
5. 作用域与生命周期:结构化并发的入口
5.1 为什么必须有 Scope
没有作用域就像“放飞无人机不设返航”:任务何时结束、失败如何收口都不清楚。
5.2 结构化并发原则
- 子协程属于父协程
- 父协程结束/取消,子协程自动结束
- 避免
GlobalScope这种脱离生命周期的“野生协程”
5.3 示例:coroutineScope
suspend fun loadUserPage(repo: Repo): UserPage = coroutineScope {
val profile = async { repo.getProfile() }
val posts = async { repo.getPosts() }
UserPage(
profile = profile.await(),
posts = posts.await()
)
}
coroutineScope 保证:任何子任务失败,整体感知并收敛。
6. Dispatcher 与线程切换策略
6.1 常见 Dispatcher
Dispatchers.Main:UI 更新Dispatchers.IO:网络、文件、数据库Dispatchers.Default:CPU 计算
6.2 推荐线程策略
- ViewModel 层在
Main - Repository 做 IO 切换
- 重计算切到
Default
6.3 示例
suspend fun loadDetail(api: Api): Detail = withContext(Dispatchers.IO) {
api.getDetail()
}
suspend fun calc(items: List<Int>): Int = withContext(Dispatchers.Default) {
items.sum()
}
7. 并发组合:launch/async/await 正确使用
7.1 何时用 launch
- 只关心执行,不关心返回值(如上报日志、触发事件)
7.2 何时用 async
- 需要结果,且可能并发等待多个结果
7.3 页面并发加载示例
suspend fun loadDashboard(api: Api): Dashboard = coroutineScope {
val profile = async { api.profile() }
val notices = async { api.notices() }
val stats = async { api.stats() }
Dashboard(
profile = profile.await(),
notices = notices.await(),
stats = stats.await()
)
}
7.4 注意
async的异常在await()时抛出- 不要“创建了 async 却忘记 await”
8. 取消与超时:可控结束比成功更重要
8.1 为什么取消重要
App 页面退出、任务过时、用户主动中断,都需要及时取消,避免浪费资源。
8.2 基础取消
val job = scope.launch {
repeat(10) {
delay(200)
println("work $it")
}
}
job.cancel()
8.3 协作式取消(CPU 任务)
scope.launch(Dispatchers.Default) {
while (isActive) {
// heavy compute
}
}
8.4 超时
val result = withTimeoutOrNull(1000) {
api.longTask()
}
9. 异常传播与 Supervisor 思维模型
9.1 默认传播模型
在普通 coroutineScope 中,某个子协程失败通常会取消同级。
9.2 CoroutineExceptionHandler 适用范围
主要对根 launch 有效;async 要在 await 处处理。
9.3 Supervisor:局部失败不拖垮全局
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val a = launch { error("A fail") }
val b = launch { delay(300); println("B still running") }
joinAll(a, b)
}
适合场景:页面多个模块并行加载,某个模块失败不影响其他模块展示。
10. Flow 入门:什么是冷流(Cold Flow)
10.1 定义
冷流:只有在被收集(collect)时才开始生产数据;每个收集者都会触发一套新的生产过程。
10.2 生活类比
冷流像“点播视频”:你点开才开始播放;每个人点开都是自己的播放进度。
10.3 示例(证明“每次 collect 都重新执行”)
fun tickerFlow() = flow {
println("flow start")
repeat(3) {
delay(300)
emit(it)
}
}
scope.launch {
tickerFlow().collect { println("collector A -> $it") }
}
scope.launch {
tickerFlow().collect { println("collector B -> $it") }
}
你会看到 flow start 打印两次,因为两次收集是两套独立执行。
10.4 适用场景
- 用户操作触发一次网络请求流
- 需要“懒执行”数据处理流水线
11. 冷流 vs 热流:StateFlow/SharedFlow 全面对比
11.1 什么是热流
热流:不依赖是否有人收集,数据源可能一直在产生数据。
生活类比:
- 热流像“广播电台”,电台一直播,听众随时加入
- 冷流像“点播节目”,你点开才播放
11.2 三者定位
Flow:冷流,按需执行StateFlow:热流,保存并提供“最新状态”SharedFlow:热流,广播事件,可配置缓存/replay
11.3 StateFlow 示例(状态)
data class UiState(
val loading: Boolean = false,
val data: String? = null,
val error: String? = null
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState
fun setLoading() {
_uiState.value = _uiState.value.copy(loading = true)
}
特点:
- 必须有初始值
- 永远有最新值
- 新订阅者会立刻拿到当前值
11.4 SharedFlow 示例(事件)
sealed interface UiEvent {
data class Toast(val message: String): UiEvent
data object NavigateBack: UiEvent
}
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 1
)
val events: SharedFlow<UiEvent> = _events
suspend fun sendToast(msg: String) {
_events.emit(UiEvent.Toast(msg))
}
特点:
- 适合一次性事件(Toast、导航、弹窗)
- 可控制 replay/buffer 行为
11.5 常见选择规则
- 页面“状态” ->
StateFlow - 页面“一次性事件” ->
SharedFlow - 计算/请求流水线 ->
Flow
12. Flow 常见操作符与背压处理
12.1 常见操作符
flowOf(1, 2, 3, 4)
.map { it * 2 }
.filter { it > 4 }
.onEach { println("emit=$it") }
.catch { e -> println("error=${e.message}") }
.collect()
12.2 高频输入场景(搜索框)
queryFlow
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { keyword -> repository.search(keyword) }
.collect { result -> render(result) }
解释:
debounce:防抖distinctUntilChanged:去重flatMapLatest:只保留最新请求(旧请求自动取消)
12.3 背压与处理策略
buffer():生产和消费解耦conflate():只保留最新值,跳过中间值collectLatest():来新值就取消上一次处理
13. Android 架构落地:ViewModel + Repository
13.1 推荐职责分层
- ViewModel:状态与事件编排
- Repository:数据获取与线程切换
- DataSource/API:具体 IO 实现
13.2 示例
class ApproveViewModel(
private val repository: ApproveRepository
): ViewModel() {
private val _state = MutableStateFlow(ApproveUiState())
val state: StateFlow<ApproveUiState> = _state
private val _events = MutableSharedFlow<String>()
val events: SharedFlow<String> = _events
fun load(procInsId: String) {
viewModelScope.launch {
_state.update { it.copy(loading = true) }
runCatching { repository.fetchDetail(procInsId) }
.onSuccess { detail ->
_state.update { it.copy(loading = false, detail = detail) }
}
.onFailure { e ->
_state.update { it.copy(loading = false) }
_events.emit(e.message ?: "加载失败")
}
}
}
}
class ApproveRepository(private val api: ApproveApi) {
suspend fun fetchDetail(id: String): ApproveDetail = withContext(Dispatchers.IO) {
api.getDetail(id)
}
}
14. Channel 与 Actor:协程间通信与串行化状态
14.1 Channel:点对点/队列通信
val channel = Channel<Int>(capacity = Channel.BUFFERED)
scope.launch {
repeat(5) { channel.send(it) }
channel.close()
}
scope.launch {
for (item in channel) {
println("receive=$item")
}
}
14.2 Actor:把共享状态写入串行化
sealed interface CounterMsg
object Inc : CounterMsg
class Get(val reply: CompletableDeferred<Int>) : CounterMsg
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var count = 0
for (msg in channel) {
when (msg) {
Inc -> count++
is Get -> msg.reply.complete(count)
}
}
}
适用:高并发下避免锁竞争,保证单线程顺序更新。
15. 高级主题:并发安全、性能优化、调试手段
15.1 并发安全
常见风险:多个协程同时修改同一可变对象。
解决方案:
- 不可变数据结构
Mutex- Actor 串行化
val mutex = Mutex()
var counter = 0
suspend fun safeInc() {
mutex.withLock {
counter++
}
}
15.2 性能优化建议
- 不要把小任务切成过多协程
- IO/CPU 任务严格分 Dispatcher
- 减少无意义线程切换
- Flow 高频链路使用
buffer/conflate/collectLatest
15.3 调试建议
- JVM 参数开启调试:
-Dkotlinx.coroutines.debug - 协程命名:
CoroutineName("LoadApproveTimeline") - 关键节点打印
currentCoroutineContext()信息
16. 协程测试:runTest、虚拟时间与可测性
推荐依赖:kotlinx-coroutines-test
@OptIn(ExperimentalCoroutinesApi::class)
class ApproveViewModelTest {
@Test
fun `load success updates state`() = runTest {
val vm = ApproveViewModel(FakeApproveRepository(success = true))
vm.load("proc-1")
advanceUntilIdle()
assert(vm.state.value.loading.not())
assert(vm.state.value.detail != null)
}
}
关键点:
runTest替代runBlockingadvanceUntilIdle()推进虚拟时间- 测试中避免真实延迟和真实网络
17. 完整业务案例:审批页并发加载 + 重试 + 超时 + 事件流
场景:进入审批详情页并发请求三类数据;允许瞬时失败重试;总时长受控;失败发事件提示。
suspend fun loadApprovePage(procInsId: String): ApprovePageData = coroutineScope {
suspend fun <T> retry(times: Int = 2, block: suspend () -> T): T {
var last: Throwable? = null
repeat(times + 1) {
try {
return block()
} catch (e: Throwable) {
last = e
delay(200)
}
}
throw last ?: IllegalStateException("unknown")
}
val detail = async {
withTimeout(3000) { retry { approveApi.getDetail(procInsId) } }
}
val dict = async {
withTimeout(3000) { retry { dictApi.getApproveDict() } }
}
val timeline = async {
withTimeout(3000) { retry { approveApi.getTimeline(procInsId) } }
}
ApprovePageData(
detail = detail.await(),
dict = dict.await(),
timeline = timeline.await()
)
}
这个例子同时体现:
- 结构化并发(
coroutineScope) - 并发请求(
async) - 超时(
withTimeout) - 重试(
retry) - 错误收敛(
await抛出)
18. 常见误区与实践清单(可直接贴团队规范)
18.1 常见误区
- 在业务层使用
GlobalScope - 主线程执行 IO
- 没有取消策略和超时策略
- 把一次性事件塞进
StateFlow async创建后忘记await- Flow 链路过长但无背压策略
18.2 实践清单
- 生命周期归属明确:
viewModelScope/lifecycleScope - Repository 统一
withContext(Dispatchers.IO) - 状态用
StateFlow,事件用SharedFlow - 关键请求设置超时、重试、兜底
- 对高频输入加
debounce + flatMapLatest - 测试统一
runTest
结语
协程真正的难点不在 API 数量,而在工程思维:
- 任务归属谁(Scope)
- 跑在哪(Dispatcher)
- 何时停(取消/超时)
- 失败怎么办(异常传播/Supervisor)
- 数据如何流动(Flow/StateFlow/SharedFlow)
如果你愿意,我下一步可以再给这份文档补两章:
- “协程面试高频题(附标准回答)”
- “结合你当前
screen/repository/utils目录的项目落地模板(可直接复制)”
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)