Kotlin 协程 取消 6 条准则

5 阅读3分钟

参考 参考

第一条准则:协程的取消需要内部的配合

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/asynctry-catch 无法捕获异步异常在协程内部捕获或使用异常处理器
使用 SupervisorJob控制异常传播范围需要独立子任务时使用
顶层异常处理器CoroutineExceptionHandler 需在顶层安装作为根协程的上下文元素