Kotlin Coroutines 取消与异常处理最佳实践
本文深入探讨 Kotlin Coroutines 的取消机制与异常处理,帮助你写出健壮的非阻塞代码。
目录
- 协程取消的基本原理
- CancellationException 的真相
- SupervisorJob vs Job:异常传播的差异
- try-catch 与 CoroutineExceptionHandler
- 最佳实践总结
1. 协程取消的基本原理
取消的触发机制
协程取消并非强制终止,而是协作式的。当调用 job.cancel() 时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| fun main() = runBlocking { val job = launch { repeat(1000) { i -> println("Working $i ...") delay(100) } } delay(250) println("Canceling job...") job.cancel() job.join() println("Job canceled!") }
|
关键点:协程必须在Suspension Point(延迟点)检查取消状态。
没有检查取消会怎样?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| launch { repeat(1000) { i -> computeHeavyStuff() } }
launch { repeat(1000) { i -> computeHeavyStuff() yield() } }
|
2. CancellationException 的真相
它是特殊的异常
1 2 3 4 5 6 7 8 9
| launch { try { delay(1000) } catch (e: CancellationException) { println("协程被取消: $e") throw e } }
|
重要特性:
CancellationException 是正常的控制流,不是错误
- 它不会触发
CoroutineExceptionHandler
- 它会传播到父协程(除非使用
SupervisorJob)
区分取消和真正异常
1 2 3 4 5 6 7 8 9
| launch { try { throw IOException("Network error") } catch (e: IOException) { println("Handle error: $e") } }
|
3. SupervisorJob vs Job:异常传播的差异
普通 Job 的异常传播
1 2 3 4 5 6 7 8 9 10 11 12
| ┌─────────────────────────────────────────────────────────────┐ │ 普通 Job │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 子协程 A (失败) ──▶ CancellationException ──▶ 父协程 │ │ │ │ │ ▼ │ │ 全部取消 │ │ │ │ 子协程 B ──▶ 也被取消(即使 B 没问题) │ │ │ └─────────────────────────────────────────────────────────────┘
|
SupervisorJob 的异常传播
1 2 3 4 5 6 7 8 9 10 11 12
| ┌─────────────────────────────────────────────────────────────┐ │ SupervisorJob │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 子协程 A (失败) ──▶ CancellationException ──▶ 父协程 │ │ │ │ │ ▼ │ │ 只取消 A 自己 │ │ │ │ 子协程 B ──▶ 继续运行(不受影响) │ │ │ └─────────────────────────────────────────────────────────────┘
|
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| val parentJob = CoroutineScope(Dispatchers.Main).launch { launch { delay(100) throw RuntimeException("A failed") } launch { delay(200) println("B never runs!") } }
val supervisor = CoroutineScope(Dispatchers.Main + SupervisorJob()) supervisor.launch { launch { delay(100) throw RuntimeException("A failed") } launch { delay(200) println("B still runs!") } }
|
4. try-catch 与 CoroutineExceptionHandler
CoroutineExceptionHandler 的作用
1 2 3 4 5 6 7
| val handler = CoroutineExceptionHandler { context, exception -> println("Caught: $exception") }
CoroutineScope(Dispatchers.Main + handler).launch { throw RuntimeException("Error!") }
|
注意:以下情况不会触发 Handler:
- 协程被取消 (
CancellationException)
launch 在 coroutineScope 内(而非 CoroutineScope)
正确组合:Handler + SupervisorJob
1 2 3 4 5 6 7 8 9 10 11 12
| val scope = CoroutineScope( Dispatchers.Main + SupervisorJob() + CoroutineExceptionHandler { _, throwable -> println("全局异常: $throwable") } )
scope.launch { taskA() } scope.launch { taskB() } scope.launch { taskC() }
|
结构化并发的重要性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| suspend fun badExample() { val scope = CoroutineScope(Dispatchers.IO) try { scope.launch { task1() } } catch (e: Exception) { } }
suspend fun goodExample() = coroutineScope { try { launch { task1() } } catch (e: Exception) { } }
|
5. 最佳实践总结
1. 使用 SupervisorJob 处理多任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class UserRepository( private val api: ApiService ) { private val scope = CoroutineScope( Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, _ -> } ) fun fetchUserAndPosts() { scope.launch { fetchUser() } scope.launch { fetchPosts() } } }
|
2. 在 suspend 函数中正确处理异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| suspend fun <T> safeCall(block: suspend () -> T): Result<T> { return try { Result.success(block()) } catch (e: CancellationException) { throw e } catch (e: Exception) { Result.failure(e) } }
val result = safeCall { api.getUser() } result.onSuccess { user -> } result.onFailure { error -> }
|
3. ViewModel 中正确使用 viewModelScope
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class MyViewModel( private val repository: MyRepository ) : ViewModel() { fun loadData() { viewModelScope.launch { val user = repository.getUser() _user.value = user } } }
|
4. 取消时清理资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| launch { try { withContext(Dispatchers.IO) { file.use { } } } catch (e: CancellationException) { file.close() throw e } }
launch { val file = openFile() try { } finally { file.close() } }
|
5. 常见错误对比
| 错误写法 |
正确写法 |
launch { /* 无取消检查 */ } |
launch { /* 定期 yield() */ } |
CoroutineScope(Dispatchers.Main).launch |
viewModelScope.launch 或 lifecycleScope.launch |
try-catch 包裹 launch |
使用 CoroutineExceptionHandler |
Job() |
SupervisorJob() 当需要独立失败 |
参考资料
相关文章: