Android 协程取消与结构化并发

举报
shenlan9755 发表于 2026/10/15 09:07:03 2026/10/15
【摘要】 Android 协程取消与结构化并发协程让异步代码更接近同步写法,但也引入了新的生命周期问题。页面销毁后协程是否还在执行、请求结果是否还会写回界面、取消是否真正停止了任务,这些都是决定代码是否稳定的关键点。结构化并发要求并发任务的生命周期与业务作用域绑定,而不是散落在各处自由运行。 一、协程作用域与生命周期绑定协程不应随意使用 GlobalScope 启动。页面级任务应绑定页面生命周期,请...

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 任务选择正确调度器,长循环主动检查取消。请求结果写回前确认有效性,异常传播符合业务语义,取消时可靠释放资源。协程不是让异步变简单,而是让异步的责任边界更清晰。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。