当前位置:首页 > 文章列表 > Golang > Go问答 > Go CompareAndSwap 循环为什么仍可能出现 ABA 问题

Go CompareAndSwap 循环为什么仍可能出现 ABA 问题

来源:17golang原创 2026-10-06 19:06:18 0浏览 收藏

会。Go 的 CompareAndSwap 原子性只保证“比较和替换”这一刻不可分割,并不记录比较线程等待期间发生过哪些中间状态。线程 T1 读到指针 A 后暂停,线程 T2 完成 A→B→A,T1 再用旧指针 A 去 CAS,仍可能返回成功;这就是 ABA。解决重点不是把循环写得更紧,而是确认共享状态是否需要携带版本或其他不会回退的身份信息。

如果 CAS 的成功条件只包含一个可回到原值的字段,那么“现在还是 A”不等于“期间一直是 A”。对可复用节点、无锁栈和资源生命周期敏感的结构,应把指针与版本号放进同一个原子状态,或改用锁、通道等更高层同步方案。
要点速览
  • atomic.Pointer[T].CompareAndSwap 比较的是旧指针与当前指针是否相同。
  • A→B→A 会隐藏中间变化,尤其危险于节点地址可复用的无锁结构。
  • 版本号必须和指针在同一个原子比较范围内更新,两个独立原子变量不能拼出可靠 CAS。
  • 修复后仍要检查节点回收、悬空访问、重试上限和退出条件。

先把 CAS 比较的对象说清楚

官方 sync/atomic 文档把 CAS 描述为:如果地址中的当前值等于 old,就写入 new 并返回 true,否则返回 false。atomic.Pointer[T] 的方法也是这个语义,只是把指针类型封装得更安全、可读。

因此,下面的条件只能回答“此刻是不是同一个指针”:它不能回答这个对象是否刚被移出链表、重新初始化,又被放回了同一个位置。ABA 的 A、B 不是某个固定的 Go 错误码,而是一次并发观察到的状态序列。

// 这个循环只把 head 的当前指针与 old 比较。
// 如果其他线程让 head 经历 A->B->A,CAS 仍可能成功。
var head atomic.Pointer[Node]

func popOnce() *Node {
    old := head.Load() // 记录本轮准备移除的节点
    if old == nil {
        return nil
    }
    next := old.next // 读取 old 的后继,生命周期必须由设计保证
    if head.CompareAndSwap(old, next) {
        return old // 成功只说明当前 head 仍等于 old
    }
    return nil // 失败后重新读取,不能复用过期 next
}

沿着两个线程还原 ABA 时序

假设无锁栈的头节点最初是 A,A 的后继是 X。T1 读取 old=A、next=X 后被调度出去。此时 T2 先把 head 从 A 换成 B,又完成一次弹出和回收流程,最后把一个节点放回头部,使 head 再次指向 A。

Go atomic.Pointer CompareAndSwap 的 ABA 时序,T1 读取 A 后 T2 将 A 改为 B 再改回 A
图1:CAS 只看到当前指针为 A 的 ABA 时序说明图,不是截图或运行证据。

T1 恢复后执行 head.CompareAndSwap(old, next)。比较器看到的仍是地址 A,于是返回成功,但 T1 手里的 next 可能已经来自旧链表,甚至对应一个已经重新初始化的节点。这里最危险的不是“CAS 失去原子性”,而是比较条件表达得不够完整。

判断普通数值循环是否真的有 ABA 风险

看到 CompareAndSwap 循环,不要立刻把所有问题都叫 ABA。对只表示计数的整数,A→B→A 未必改变业务含义;而下面几类状态通常要重点排查:

共享状态真正要确认的风险优先措施
无锁栈/队列节点指针节点被移除、复用后地址或指针又相同指针+版本,配合安全回收
资源句柄或槽位旧句柄看起来仍有效,但所有权已换过一轮代数、租约或锁保护
纯计数器只关心数值增减,不关心对象身份先验证不变量,通常不必套指针方案

另一个常见误区是把两个独立原子变量当成一个整体:先 CAS 指针,再递增版本号。线程可能在两步之间观察到半更新状态,ABA 仍然存在。完整比较对象必须一次性提交。

用值与版本号一起做 CAS

可靠的设计是把状态表示为不可回退的组合,例如 (指针=A, version=1)。即使指针经历 A→B→A,版本也会变成 3,等待中的线程拿着旧组合 (A,1) 再比较就会失败。

Go CAS 版本化状态的指针与版本号绑定关系,A1 与 A3 的版本差异阻止 ABA
图2:指针与版本号绑定到同一原子状态的结构说明图,不是截图或运行证据。

Go 标准库提供的是多种单值原子类型和原子指针;“指针+版本”的打包方式要结合目标架构、对齐、内存回收策略和实现复杂度谨慎设计。若无法让组合状态在一次原子操作中完成,宁可使用互斥锁保护整个状态,也不要用两个 CAS 拼接出看似无锁的协议。

// 伪代码展示比较目标:业务实现需选择可一次原子更新的状态载体。
type Stamp struct {
    ptr *Node  // 当前节点身份
    ver uint64 // 每次状态变更递增,不能随节点回退
}

func tryAdvance(old, next Stamp) bool {
    next.ver = old.ver + 1 // A->B->A 也必须留下不同的代数
    // 这里要求 CAS 同时比较 old.ptr 和 old.ver。
    // 不能先 CAS 指针,再单独 Store 版本号。
    return compareAndSwapWholeStamp(old, next)
}

补上回收、重试和退出边界

版本号只能解决“比较条件看不见中间变化”,不能自动解决节点已经被回收后的访问安全。实现无锁结构时还要回答三个问题:读到节点后谁负责延长其生命周期;节点何时允许复用;CAS 连续失败时是否有退避和退出条件。

排查时可以先画出状态不变量:成功弹出后 head 必须指向原后继,已发布节点不能被提前复用,失败重试必须重新 Load。再用 go test -race 发现数据竞争,用压力测试覆盖 T1/T2 的暂停窗口;Race 检测通过也不代表 ABA 协议正确,它们解决的是不同层面的问题。

相关问题

CompareAndSwap 返回 true 就代表整个操作安全了吗?

不代表。它只证明指定地址在比较瞬间等于 old,并且替换成功;节点生命周期、业务不变量和后续读取仍需独立保证。

把指针改成整数就能避免 ABA 吗?

不能。只要整数也可能回到旧值,比较条件就可能隐藏中间变化。关键是加入不可回退的代数或改用整体锁保护。

为什么不直接用 mutex?

如果临界区短、竞争可控且正确性优先,mutex 往往更容易审查。只有明确测量到锁竞争成本,并且能证明回收与内存序协议时,才值得维护无锁实现。

CAS 循环失败后应该怎么重试?

重新 Load 当前整体状态,再计算 next;不要继续使用上一次失败的后继指针。连续失败还应设置退避、上限或降级路径。

把问题归纳成一句话:CAS 保证的是一次比较替换,不是历史证明。只要业务需要区分“从未变化的 A”和“变化后又回到 A”,就必须把版本、所有权或更高层同步机制纳入成功条件。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python 3.14.7 文档更新后该关注哪些兼容项Python 3.14.7 文档更新后该关注哪些兼容项
上一篇
Python 3.14.7 文档更新后该关注哪些兼容项
PHP session_set_cookie_params 怎么配置 SameSite
下一篇
PHP session_set_cookie_params 怎么配置 SameSite
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    368次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    426次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    442次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    390次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    217次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码