协程容易被讲成「轻量级线程」,但真正决定它好不好用的是结构化并发——用作用域把协程的生命周期、取消和异常框在一棵树上,从而不泄漏、不拖死、不吞异常。

下面按使用顺序展开:作用域与 Job 生命周期 → 上下文与调度 → 启动取消与异常 → 并发安全 → Channel 与多路复用。


一、作用域与 Job 生命周期:结构化并发不泄漏

协程作用域规定子协程的生命周期与取消边界。coroutineScope 会等所有子任务结束,其中一个失败则取消兄弟;supervisorScope 让子失败互不影响,适合 UI 上并列请求。Android 用 viewModelScopelifecycleScope 绑定组件销毁,GlobalScope 会泄漏。子协程默认继承父 Job 与 Dispatcher,withContext 只换调度器不换 Job 树。自定义 CoroutineScope 要在 onDestroy 里 cancel;structured concurrency 的要点是:不要把 Job() 造成孤儿根。测试用 runTest,生产避免在作用域外 launch 后不管。

Job 描述协程的生命周期:NewActiveCompletingCancellingCancelledCompletedlaunch 默认立即 ActiveCompletableJob 可手动 complete。父 Job 完成前会等所有子 Job,取消则向下传播。join 挂起直到终态,invokeOnCompletion 注册一次性回调。isActive/isCompleted/isCancelled 是观察窗口,不要用它们做同步锁。SupervisorJob 切断子失败向父传播,适合并列任务。结构化并发的核心是「不泄漏 Job」:Android 用 viewModelScope 绑定销毁,测试用 runTest 的 scheduler。出现僵尸协程时先查 Job 树:谁是 parent、是否被 Job() 孤立成根。

什么时候用:任何需要「一组任务一起生一起死」的地方都用作用域包住;并列且互不拖累的独立任务用 supervisorScope,请求链路上的串行依赖用普通 coroutineScope


二、上下文与调度:用 + 叠加元素,调度器决定 resume 线程

CoroutineContext 是协程的元素集合,用 + 叠加。常见元素包括 Job(生命周期)、CoroutineDispatcher(线程)、CoroutineName(调试名)和 CoroutineExceptionHandler。子协程默认继承父上下文,withContext 只替换指定元素。调度器决定 resume 发生在哪条线程:Main.immediate 适合已在主线程时避免再 post,IO 适合阻塞调用,Default 适合 CPU。不要把 Dispatcher 当锁;共享状态仍要同步。自定义元素需实现 CoroutineContext.Element。Android 里 viewModelScope 已带 SupervisorJob + Main.immediate。排查问题时先打印 coroutineContext,确认 Job 是否已取消、Dispatcher 是否被意外覆盖。

什么时候用:在主线程恢复 UI 用 Main.immediate;文件/网络阻塞用 IO;CPU 密集用 Default。需要临时换线程就用 withContext,不要新开作用域。


三、启动与取消:协作式取消与启动模式

launch 返回 Job 做「开火不管」,async 返回 Deferred 要 await 才拿到结果或异常。启动模式有 DEFAULTLAZYATOMICUNDISPATCHEDLAZYstart/joinUNDISPATCHED 在当前线程跑到第一个挂起点。取消是协作式的:cancel 后遇到 delay/yield 或检查 isActive 才会停;CPU 死循环必须自己判断。cancelAndJoin 等清理完成。withTimeout 超时抛 TimeoutCancellationException。父取消会取消子。资源释放放 finally,必要时 withContext(NonCancellable)。Android 页面销毁靠作用域自动 cancel,不要另开无主 launch。

两个容易踩的坑

  • CPU 死循环不会自己停cancel 只在遇到挂起点或检查 isActive 时生效,纯计算循环要自己判断。
  • 资源释放放 finally。真正需要「即使被取消也要做完」的清理,用 withContext(NonCancellable) 包住。

什么时候用:开火不管的任务用 launch;要拿结果的用 async;延迟到真正需要时才初始化的用 LAZY


四、异常处理:异常沿作用域向上传播

Kotlin 协程异常默认沿作用域向上传播到父协程。CoroutineExceptionHandler 只对启动体的根协程生效,async 的异常在 await/awaitAll 时抛出。SupervisorJob 让子失败不取消兄弟,适合 UI 范围多个独立任务。try/catch 会住当前挂起点的异常,但住不住子协程 launch 里的崩溃。CancellationException 是正常取消信号,不要当业务错误打点。withContext 的异常也会传到调用方。

业务上建议:网络层转 Result,ViewModel 用 SupervisorJob + Handler 收尾,避免在深处 catch 后吞掉。测试用 runTest 验证失败不会拖死整个作用域。

什么时候用:UI 上多个并列请求用 SupervisorJob 隔离失败;需要在根协程统一兜底时用 CoroutineExceptionHandlerasync 的结果要在 await 处处理异常。


五、并发安全:用结构化共享状态代替锁

Kotlin 协程并发安全不靠线程锁,而是用结构共享状态。多个协程读写同一变量时,MutexSemaphoreAtomicactor/单线程 Dispatcher 都能用。Mutex.lock 是挂起点,不要在持锁时调阻塞 IO,也不要把普通线程锁和协程 Mutex 混用。StateFlow/SharedFlow 可以把状态收到流里,但改值要在同一线程或用 update。避免在多个 Dispatchers.Default 上直接 ++ 计数器。withContext 切线程不会自动带锁。测试可用 runTestStandardTestDispatcher 控制时间。产线常见死锁是 Mutex 再进入另一个也要同一 Mutex 的 withContext。优先考虑不共享可变状态。

三个容易踩的坑

  • 持 Mutex 时别调阻塞 IOMutex.lock 是挂起点,里面塞阻塞调用会卡住协程。
  • 别把线程锁和协程 Mutex 混用。二者语义不同,混用极易死锁。
  • 改共享状态要收口StateFlow/SharedFlowupdate 要在同一线程,计数器不要在多 Default 上直接 ++

什么时候用:先考虑不共享可变状态;必须共享时小临界区用 Mutex,纯计数用 Atomic,需要串行处理消息用 actor 或单线程 Dispatcher。


六、Channel 与多路复用:select 等多个事件之一

Channel 是协程间的管道:Rendezvous 无缓冲、Buffered 有容量、CONFLATED 只保最新、UNLIMITED 不限队列。send 在缓冲满时挂起,receive 在空时挂起。关闭用 close,消费端用 for 遍历或 consumeEach。生产者-消费者模型里要明确谁关闭 Channel,避免双方互等。fan-out 可多协程 receive,fan-in 可多协程 send 到同一 Channel。与 Flow 相比,Channel 是热流且每个元素只被一个接收者拿走。取消时 send/receive 会抛 CancellationException,要用与资源清理结合。能用 callbackFlow/channelFlow 的场景优先走 Flow,仅在需要精确背压与多消费者竞争时直接暴露 Channel。

多路复用体现在 select 与 Channel。一个协程可同时挂起在多个 onReceive/onSend 上,谁先就绪谁走,其余被取消。这比多线程里 select/轮询更轻,因为挂起不占线程。典型用法是 timeout 加业务 Channel:超时走 onTimeout 分支。selectUnbiased 可避免始终优先第一个分支。注意 select 里不要再嵌套长时间挂起,否则难以取消其他 clause。

三个容易踩的坑

  • 多路复用不等于并发计算select 解决的是「等多个事件中的一个」,CPU 密集仍要用多 Dispatcher 工作者;能用 async+awaitAll 表达的并行就不必上 select
  • Channel 谁关要想清,生产者-消费者互等会死锁。
  • selectUnbiased 避偏袒,否则第一个分支总是被优先。

什么时候用:需要多消费者竞争同一数据流或精确背压时直接暴露 Channel;需要「超时或任意一路先到」时用 select;否则优先 Flow。


附:协程速查表

想做的事 用什么 不要用什么
一组任务一起生一起死 coroutineScope GlobalScope、作用域外 launch
并列任务互不拖累 supervisorScope + SupervisorJob 普通作用域(一崩全取消)
绑定组件生命周期 viewModelScope / lifecycleScope 手动 Job() 成孤儿根
主线程恢复 UI Main.immediate 把 Dispatcher 当锁
阻塞 IO Dispatchers.IO Dispatchers.Default
开火不管 launch 需要结果却用 launch
拿结果 async + await 滥用 asynclaunch
延迟初始化 LAZY 启动模式 无所谓地 DEFAULT
统一兜底异常 根协程 CoroutineExceptionHandler 在深处 catch 吞掉
共享可变状态 Mutex / Atomic / actor 线程锁混用、多 Default 直接 ++
多路等一个事件 select + onTimeout select 里嵌套长挂起
多消费者竞争数据流 Channel 能用 Flow 还硬暴露 Channel