参考 参考
第一条准则:协程的取消需要内部的配合
fun main() = runBlocking {
val job = launch {
var i = 0
while (true) {
// ❌ 错误:未检查取消状态
println(i++)
delay(100)
}
}
delay(500)
job.cancel() // 取消请求
job.join() // 等待结束
println("Canceled") // 实际不会执行,协程仍在运行!
}
**问题分析:**
- 协程内部没有响应取消请求
- `isActive` 未被检查,`delay()` 是唯一可取消点
- 结果:协程无法被取消,继续运行导致资源泄漏
** 正确实现:**
val job = launch {
while (isActive) { // ✅ 检查取消状态
println(i++)
try {
delay(100) // 可取消的挂起点
} catch (e: CancellationException) {
// 清理资源
throw e // 重新抛出
}
}
}
第二条准则:不要轻易打破协程的父子结构
val scope = CoroutineScope(Job())
fun startChild() {
// ❌ 错误:使用全局作用域打破父子关系
GlobalScope.launch {
delay(1000)
println("Child done")
}
}
fun main() = runBlocking {
val parentJob = scope.launch {
startChild()
delay(2000)
println("Parent done")
}
delay(500)
scope.cancel() // 取消父作用域
parentJob.join()
// 输出:"Child done" - 子协程未被取消!
}
**问题分析:**
- 使用 `GlobalScope` 创建的子协程脱离父子结构
- 父作用域取消时,子协程继续运行
- 破坏结构化并发原则
// ✅ 保持父子关系
fun CoroutineScope.startChild() {
// 使用当前作用域
launch {
delay(1000)
println("Child done")
}
}
第三条准则:正确处理 CancellationException
val job = launch {
try {
delay(1000)
} catch (e: Exception) {
// ❌ 错误:捕获所有异常但不处理取消
println("Caught: $e")
}
println("Completed") // 错误地继续执行
}
delay(100)
job.cancel()
**问题分析:**
- 捕获 `CancellationException` 后未重新抛出
- 协程错误地继续执行而非取消
- 破坏结构化取消机制
try {
delay(1000)
} catch (e: CancellationException) {
// ✅ 处理清理逻辑
println("Cancelling...")
throw e // 必须重新抛出
} catch (e: Exception) {
// 处理业务异常
}
第四条准则:不要用 try-catch 直接包裹 launch/async
try {
// ❌ 错误:外部 try-catch 无效
scope.launch {
throw RuntimeException("Boom!")
}
} catch (e: Exception) {
println("Caught: $e") // 永远不会执行
}
// 结果:应用崩溃!
**问题分析:**
- `launch` 调用本身几乎不抛异常
- 协程体在异步线程执行,异常无法跨线程传播
- 外部 try-catch 只能捕获同步异常
// ✅ 在协程内部捕获
scope.launch {
try {
throw RuntimeException("Boom!")
} catch (e: Exception) {
println("Caught internally: $e")
}
}
// ✅ 使用 CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, e ->
println("Handler caught: $e")
}
scope.launch(handler) { ... }
第五条准则:灵活使用 SupervisorJob
val scope = CoroutineScope(Job()) // ❌ 普通 Job
scope.launch {
launch {
delay(100)
throw RuntimeException("Child 1 failed")
}
launch {
delay(200)
println("Child 2 completed") // 永远不会执行
}
}
// 结果:所有子协程立即取消
**问题分析:**
- 普通 Job:一个子协程失败导致整个作用域取消
- 需要并行执行的子任务互相影响
// ✅ 使用 SupervisorJob
val scope = CoroutineScope(SupervisorJob())
scope.launch {
launch {
// 失败不影响兄弟协程
throw RuntimeException("Child 1 failed")
}
launch {
delay(200)
println("Child 2 completed") // 正常执行
}
}
第六条准则:正确使用 CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, e ->
println("Handler caught: $e")
}
// ❌ 错误:在非顶层协程使用
scope.launch(handler) {
launch {
throw RuntimeException("Inner failure")
}
}
// 结果:异常未被处理!应用崩溃
**问题分析:**
- 异常处理程序需安装在**顶层协程**
- 子协程异常会传播到父协程,但父协程未安装处理器
// ✅ 正确:在顶层协程安装
scope.launch(handler) { // 顶层协程
// 异常会传播到此
throw RuntimeException("Top-level failure")
}
// ✅ 或直接在子协程安装(需是子树的顶层)
scope.launch {
launch(handler) { // 子树的顶层
throw RuntimeException("Handled")
}
}
总结表:六大准则实践要点
准则 | 关键点 | 正确实践 |
---|---|---|
取消需要配合 | 检查 isActive 或 ensureActive() | 在循环中检查状态,处理 CancellationException |
保持父子结构 | 避免使用 GlobalScope | 通过 coroutineScope 或传递当前作用域 |
处理取消异常 | 不吞噬 CancellationException | 清理后重新抛出,区分业务异常 |
不直接包裹 launch/async | try-catch 无法捕获异步异常 | 在协程内部捕获或使用异常处理器 |
使用 SupervisorJob | 控制异常传播范围 | 需要独立子任务时使用 |
顶层异常处理器 | CoroutineExceptionHandler 需在顶层安装 | 作为根协程的上下文元素 |