Android 协程取消与结构化并发
Android 协程取消与结构化并发
协程让异步代码更接近同步写法,但也引入了新的生命周期问题。页面销毁后协程是否还在执行、请求结果是否还会写回界面、取消是否真正停止了任务,这些都是决定代码是否稳定的关键点。结构化并发要求并发任务的生命周期与业务作用域绑定,而不是散落在各处自由运行。
一、协程作用域与生命周期绑定
协程不应随意使用 GlobalScope 启动。页面级任务应绑定页面生命周期,请求结果只在界面仍然存在时更新。
class ProductListViewModel : ViewModel() {
fun loadProducts() {
viewModelScope.launch {
val result = productRepository.listProducts()
render(result)
}
}
}
viewModelScope 会在 ViewModel 清理时自动取消任务,是页面数据加载的常见选择。自定义作用域必须显式管理取消时机。
二、取消是协作式的
调用 cancel() 并不会立刻终止正在执行的代码。协程只有在挂起点检查取消状态,或主动检查 isActive 时才会停止。
suspend fun processRows(rows: List<Row>) {
for (row in rows) {
ensureActive()
processRow(row)
}
}
长循环内部应加入 ensureActive()。如果计算在单个函数内持续执行且没有挂起点,外部取消不会生效,任务会继续占用资源。
三、CPU 密集任务使用默认调度器
Dispatchers.Default 面向 CPU 密集型任务,Dispatchers.IO 面向阻塞式 IO。不要把所有任务都切到 IO 线程。
suspend fun parseRows(bytes: ByteArray): List<Row> {
return withContext(Dispatchers.Default) {
parser.parse(bytes)
}
}
阻塞式网络、文件和数据库 API 不应直接在主线程或默认调度器上执行,应使用项目统一的异步封装。
四、请求结果过期的防护
用户快速切换筛选条件时,旧请求可能晚于新请求返回。每个请求应持有标识,写回前确认仍然有效。
private var loadToken = 0
fun loadProducts(keyword: String) {
val token = ++loadToken
viewModelScope.launch {
val result = productRepository.listProducts(keyword)
if (token == loadToken) {
render(result)
}
}
}
如果项目已有请求取消机制,应优先取消旧请求,而不是只依赖结果丢弃。
五、不要用 Thread.sleep 阻塞协程
在协程中使用 Thread.sleep 会阻塞当前线程,并让取消无法及时生效。
suspend fun retryDelay(millis: Long) {
delay(millis)
}
delay 是挂起函数,线程可以被复用,取消时也会及时响应。任何阻塞调用都应明确所在调度器,并评估对线程池的影响。
六、异常会沿结构化并发传播
子协程抛出未捕获异常时,会影响同一作用域内的其他任务。启动方式应根据业务语义选择。
scope.launch {
loadHeader()
}
scope.launch {
loadContent()
}
如果多个任务彼此独立、允许一个失败不影响其他任务,应在各自协程内处理异常,而不是让异常直接传播到父作用域。
七、supervisorScope 隔离子任务失败
页面并行加载多个模块时,一个模块失败不应导致所有模块取消。
suspend fun loadPage() = supervisorScope {
val header = async { loadHeader() }
val content = async { loadContent() }
renderHeader(header.await())
renderContent(content.await())
}
使用 supervisorScope 时,每个子任务的失败只影响自身,调用方仍需明确失败后的展示策略。
八、Flow 收集要绑定生命周期
UI 层收集 Flow 时应使用生命周期感知方式,页面离开前台后停止收集,恢复时继续。
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
render(state)
}
}
}
不要在 init 中无限收集。长期运行的流需要明确停止条件,否则会随着页面重建不断累积。
九、取消时的资源清理
任务取消前可能需要关闭文件、停止下载或释放资源。使用 try/finally 保证清理执行。
suspend fun downloadFile(target: File) {
val output = target.outputStream()
try {
networkSource.copyTo(output)
} finally {
output.close()
}
}
清理代码本身应避免再次调用可挂起的业务函数,必要时使用 withContext(NonCancellable) 完成关键释放。
十、测试并发行为
协程测试不应只验证正常返回。应覆盖取消、切换页面、慢请求返回和异常传播。
- 页面销毁后任务是否停止。
- 快速连续请求是否不会写入旧结果。
- 子任务失败是否影响其他模块。
- 长循环是否响应取消。
- 资源清理是否在取消后执行。
总结
结构化并发的核心是任务生命周期与业务作用域一致。页面任务使用生命周期感知作用域,CPU 与 IO 任务选择正确调度器,长循环主动检查取消。请求结果写回前确认有效性,异常传播符合业务语义,取消时可靠释放资源。协程不是让异步变简单,而是让异步的责任边界更清晰。
- 点赞
- 收藏
- 关注作者
评论(0)