You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

协程中取消长耗时库函数时如何避免竞态条件

问题核心原因

该竞态的本质是取消信号的检查、长耗时库函数的启动、取消方法的调用三个操作没有原子性保证:普通布尔标志位(哪怕是volatile修饰的)只能保证内存可见性,标志位检查和后续函数启动之间存在不可消除的时间窗口,cancel信号可能刚好在这个窗口内触发,既没拦住函数启动,也没来得及调用库的取消方法。

正确实现方案

用原子状态机消除所有竞态窗口,不需要额外加重量级锁,性能开销极低,完全适配协程场景:

  1. 定义明确的生命周期状态,标记长耗时库函数的运行阶段
  2. 所有状态流转都用CAS(比较并交换)原子操作完成,不存在中间态
  3. 启动长耗时函数前先原子确认无取消信号,收到取消信号时根据当前状态判断是否需要调用库的取消方法

实现代码

import java.util.concurrent.atomic.AtomicReference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

// 长耗时库函数的生命周期状态
private enum class LibraryTaskState {
    IDLE,       // 未启动
    RUNNING,    // 运行中
    CANCELLED,  // 已收到取消请求
    FINISHED    // 已执行完成
}

class TaskWrapper(private val scope: CoroutineScope) {
    private val taskState = AtomicReference(LibraryTaskState.IDLE)

    fun cancel() {
        // 原子性标记取消,同时拿到修改前的状态
        val prevState = taskState.getAndSet(LibraryTaskState.CANCELLED)
        // 只有当任务已经在运行时,才需要调用库的取消方法
        if (prevState == LibraryTaskState.RUNNING) {
            cancelSomeLongRunningCancellableLibraryFunction()
        }
    }

    fun run() {
        scope.launch(Dispatchers.Default) {
            someLongRunningFunction()
            
            // 原子性尝试标记任务为运行中:只有当前状态是IDLE(没收到取消)才会成功
            val canStartTask = taskState.compareAndSet(LibraryTaskState.IDLE, LibraryTaskState.RUNNING)
            if (!canStartTask) {
                // 已经收到取消信号,直接跳过,不启动长耗时任务
                return@launch
            }

            try {
                someLongRunningCancellableLibraryFunction()
            } finally {
                // 任务执行完(不管是正常结束还是被取消),标记为完成
                taskState.set(LibraryTaskState.FINISHED)
            }
        }
    }

    // 以下是外部依赖的库函数,不可修改
    private fun someLongRunningFunction() {}
    private fun someLongRunningCancellableLibraryFunction() {}
    private fun cancelSomeLongRunningCancellableLibraryFunction() {}
}
逻辑验证(覆盖所有时序场景)
  • 场景1:cancel调用时,协程还在执行someLongRunningFunction():cancel会先把状态设为CANCELLED,等协程执行到启动前的CAS检查时,会发现状态不是IDLE,直接跳过启动长耗时函数,不会出现漏取消。
  • 场景2:协程刚完成CAS标记为RUNNING,还没正式启动长耗时函数时cancel触发:cancel拿到的前序状态是RUNNING,会立刻调用库的取消方法,由于库的取消方法支持跨线程调用,哪怕函数刚完成启动,也能被正常中止。
  • 场景3:长耗时函数已经执行完成后才调用cancel:此时状态是FINISHED,cancel不会触发库的取消方法,不会产生无效调用。

注意:如果cancelSomeLongRunningCancellableLibraryFunction对未启动/已结束的任务调用是幂等安全的,甚至可以省略状态判断直接调用,进一步简化逻辑,但用状态机过滤可以避免不必要的跨线程调用开销。

内容的提问来源于stack exchange,提问作者Steve M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 05:33:16