导读:本期聚焦于小黄人创作的《C++中如何使用std::atomic_compare_exchange?原子操作CAS用法详解》,敬请观看详情。CAS即Compare And Swap,是一种广泛用于无锁编程的原子操作。C++标准库提供了std::atomic_compare_exchange_weak和std::atomic_compare_exchange_strong两个函数来实现这一能力。本文围绕这两个函数展开讲解,包括参数含义、期望值与目标值的比较逻辑、交换失败时的行为差异、memory_order内存序的选择策略,并通过自旋锁和无锁计数器等完整代码示例演示实际用法。同时分析了ABA问题、伪共享等无锁编程中常见的坑,帮助你在多线程环境下写出正确高效并发代码。

在多线程编程中,当我们需要在不加锁的情况下修改共享数据,CAS(Compare And Swap)几乎是绕不开的核心技术。C++11引入的std::atomic系列原子类型为CAS提供了语言层面的支持,其中std::atomic_compare_exchange_weak和std::atomic_compare_exchange_strong是两个最常用也最容易用错的函数。这篇文章将系统讲解它们的用法、区别以及实际工程中需要注意的细节。

C++中如何使用std::atomic_compare_exchange?原子操作CAS用法详解

CAS的基本原理与函数签名

CAS的思想非常直观:先读取一个内存位置的当前值作为期望值,然后在后续某个时刻尝试把这个内存位置的值从期望值改成新值。如果在此期间没有其他线程修改过这块内存,修改成功;否则说明值已经变了,修改失败,通常需要重新读取并重试。整个过程由硬件指令(如x86的cmpxchg)保证原子性,中间不会被打断。

C++标准库中,CAS相关的函数有两个变体,它们的函数签名如下:

bool atomic_compare_exchange_weak(std::atomic<T>* obj, T* expected, T desired);
bool atomic_compare_exchange_strong(std::atomic<T>* obj, T* expected, T desired);

// 也经常以成员函数形式调用:
bool std::atomic<T>::compare_exchange_weak(T& expected, T desired);
bool std::atomic<T>::compare_exchange_strong(T& expected, T desired);

这三个参数的含义需要特别留意,尤其是第二个参数expected,它既是输入也是输出。执行逻辑是:原子地比较*objexpected的值,如果相等,就把desired写入*obj并返回true;如果不相等,就把*obj的当前实际值写回expected,并返回false。也就是说,函数返回false时,expected已经被更新成了内存中的最新值,这一点在编写重试循环时非常方便,可以直接用更新后的expected继续参与下一轮计算,省去一次额外的load操作。

weak与strong的区别及正确选择

很多初学者第一次接触这两个函数时都会困惑:既然有strong版本,为什么还要存在weak版本?关键区别在于允许的伪失败(spurious failure)。weak版本即使当前值与期望值相等,也可能返回false,也就是所谓“莫名其妙地失败了”。而strong版本保证只要值相等就一定成功。

weak版本的存在是为了性能。在某些硬件平台(典型的如ARM)上,CAS底层是通过LL/SC指令对(load-linked/store-conditional)实现的,而SC指令天然可能在任意时刻失败,例如线程在两次指令之间被中断、或其他核碰巧写入了同一缓存行。在这些平台上,weak版本可以直接映射到一条原生指令循环之外的路径,性能更好。

使用准则可以简单归纳为一句话:在循环中做CAS就用weak,单独调用一次且不重试就用strong。看下面这个典型的无锁累加例子:

std::atomic<int> value{0};

void add_ten()
{
    int old = value.load(std::memory_order_relaxed);
    // weak版本可能伪失败,但在循环中无所谓,失败会自动重试
    while (!value.compare_exchange_weak(old, old + 10,
                                        std::memory_order_acq_rel,
                                        std::memory_order_relaxed)) {
        // 循环体为空:old已经被函数更新为最新值,直接进入下一轮
    }
}

注意上面代码中失败时循环体是空的,这正体现了expected参数会被回写的特性。如果用strong版本也能得到正确结果,只是在不支持硬件级强CAS的平台上会多付出一些开销。反过来,如果你写的是“只尝试一次、失败就走别的分支”的逻辑,用weak就可能出错,因为伪失败会导致程序误以为发生了竞争。

内存序的选择策略

完整的成员函数签名还支持两个内存序参数,分别是成功时和失败时使用的内存序:

bool compare_exchange_strong(T& expected, T desired,
                             std::memory_order success,
                             std::memory_order failure);

默认情况下两个内存序都是memory_order_seq_cst,这是最强的顺序一致性保证,正确性最容易推理,但性能也最差。在性能敏感的场景中,合理降低内存序可以显著减少内存屏障带来的开销。常见的搭配方案有:纯计数器场景用relaxed(只要求原子性,不涉及同步关系);实现锁或发布数据时成功路径用acq_relacquire,失败路径用acquirerelaxed

有一条约束必须记住:failure的内存序不能强于success,且不能是release或acq_rel。这是因为失败的CAS本质上只是一次load操作,不存在“发布”语义。例如写成成功用acquire、失败用seq_cst就是编译错误。

一个实用的自旋锁实现可以说明内存序的用法:

class SpinLock {
    std::atomic<bool> locked_{false};
public:
    void lock()
    {
        // exchange成功获取锁,release保证临界区写入对解锁方可见
        while (locked_.exchange(true, std::memory_order_acquire)) {
            // 自旋等待,可插入pause指令降低总线争用
        }
    }
    void unlock()
    {
        locked_.store(false, std::memory_order_release);
    }
};

如果把CAS换成compare_exchange_weak的写法(期望false、写入true),配合acquire/release内存序,逻辑同样是成立的。这里成功获取锁用acquire是为了保证后续临界区内的读写不会重排到加锁之前,解锁用release则保证临界区的修改对下一个加锁线程可见。内存序一旦用错,即使测试通过也可能在特定架构上出现难以复现的诡异bug,所以拿不准时先用默认的seq_cst,确认正确后再针对性优化。

实战中的常见坑:ABA问题与伪共享

无锁算法最著名的陷阱是ABA问题。CAS只比较值是否相等,而无法感知值是否“变过”。假设线程1读到指针A,线程2把A改成B又改回A,线程1的CAS依然会成功,但它操作的可能已经是一个被回收重用的对象,在带垃圾回收的语言里问题不大,在C++中手动管理内存时就是悬垂指针的灾难。常见的应对方案包括:给数据附加版本号tagged pointer,每次修改递增版本,比较时同时比较值和版本;或使用hazard pointer、epoch-based reclamation等延迟回收机制。

另一个容易被忽视的坑是伪共享(false sharing)。多个线程频繁对位于同一缓存行的不同atomic变量做CAS,会导致缓存行在核间不停弹跳,性能急剧下降,有时甚至比加锁还慢。排查方法是观察多线程下的性能不随核数扩展,解决思路是用alignas(64)把高频写的原子变量按缓存行大小对齐,让每个变量独占一个缓存行:

struct alignas(64) PaddedCounter {
    std::atomic<uint64_t> count{0};
    // 填充至64字节,避免与其他计数器共享缓存行
    char pad[64 - sizeof(std::atomic<uint64_t>)];
};

此外还有两点建议:第一,CAS重试循环在高竞争下会造成大量CPU空转,写入冲突严重时 Mutex 反而更划算,不要为了“无锁”而无锁;第二,使用TSan(ThreadSanitizer)或helgrind工具对并发代码做检测,能在开发阶段暴露大部分内存序错误。CAS是把双刃剑,理解清楚硬件语义和内存模型之后,它才能发挥出真正的威力。

std::atomic_compare_exchangeCAS原子操作C++无锁编程修改时间:2026-09-12 22:26:47

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260912/55595.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。