跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

C++ CAS 原子操作深度解析与实战

C++ CAS 原子操作是并发编程的核心基石。内容涵盖 compare_exchange_weak 与 strong 的硬件差异及适用场景,通过无锁栈代码实例展示标准循环模式。针对 ABA 问题,解析指针复用风险并提供版本号解决方案。此外,对比自旋锁与互斥锁在等待方式、上下文切换及 CPU 占用上的性能表现,帮助开发者根据临界区特性选择合适同步原语。

BigDataPan发布于 2026/3/23更新于 2026/5/15 浏览
C++ CAS 原子操作深度解析与实战

在 C++ 中,CAS(Compare-And-Swap)操作主要通过 <atomic> 头文件中的 std::atomic 类模板提供的成员函数来实现。理解其底层逻辑对于编写高性能并发代码至关重要。

1. CAS 的核心逻辑

CAS 操作本质上包含三个关键要素:

内存值 (V):要更新的变量当前存储的值。 预期原值 (E, Expected):线程读取时认为该变量应该有的值(通常是之前的快照)。 新值 (N, New):线程希望写入的新数据。

操作流程遵循比较、交换、重试的循环模式:

  1. 比较:检查内存位置 V 的当前值是否等于预期值 E。
  2. 交换(如果相等):若 V == E,说明在此期间没有其他线程修改过该变量,则将 V 更新为新值 N。操作成功,返回 true。
  3. 重试/失败(如果不相等):若 V != E,说明有其他线程抢先修改了变量。此时不进行更新,并将内存中的最新值 V 赋值给预期值 E(这样下次重试时就有了最新的参照)。操作失败,返回 false。
2. C++ 中的 CAS 函数

std::atomic 提供了两个版本的 CAS 实现,选择哪一个取决于具体场景。

A. compare_exchange_weak(弱 CAS)

即使内存中的值与预期值相等,它也可能返回 false,这被称为'伪失败'。

原因在于某些硬件架构(如 ARM、PowerPC)上,CAS 是通过 LL/SC(Load-Linked/Store-Conditional)指令实现的。上下文切换或其他中断可能导致 SC 指令意外失败。

适用场景:循环算法。因为在循环中,伪失败只会导致多一次循环迭代,而 weak 版本在某些平台上通常比 strong 版本性能更高。

B. compare_exchange_strong(强 CAS)

只有当内存值确实不等于预期值时才会返回 false。

适用场景:非循环的操作,或者当重试的代价非常昂贵时。它在内部可能已经包含了一个小循环来处理伪失败。

3. 代码示例:标准的 CAS 循环

这是最经典的使用模式,用于原子地修改一个变量,例如向栈中压入一个元素。

#include <atomic>
#include <iostream>

struct Node {
    int data;
    Node* next;
};

std::atomic<Node*> head(nullptr);

  {
    Node* new_node =  Node{data, };
    
    
    Node* expected = head.();
    
     {
        
        new_node->next = expected;
        
        
        
        
    }  (!head.(expected, new_node));
}
void
push
(int data)
new
nullptr
// 步骤 1: 获取当前头节点的快照
load
do
// 步骤 2: 构造新状态基于快照
// 步骤 3: 尝试原子交换
// 如果 head == expected,则 head = new_node,返回 true,退出循环
// 如果 head != expected,则 expected = head (自动更新),返回 false,继续循环
while
compare_exchange_weak

这里需要特别注意 expected 是 T& 引用类型。当 CAS 失败时,函数不仅返回 false,还会自动将内存中最新的实际值写入 expected 变量中,这正是我们需要的行为——让下一次循环使用最新的状态。

4. ABA 问题

ABA 问题是指在使用 CAS 操作时,一个变量的值经历了 A -> B -> A 的变化过程,但 CAS 仅仅检查值是否相等,因此误认为该变量从未被修改过,从而导致逻辑错误。

核心本质在于 CAS 只能检测'状态'是否一致,无法检测'时序'是否发生过变化。

我们可以举一个例子来形象地形容:桌子上有一杯水(状态 A)。你看到桌上有杯水,转身去接电话。室友过来把水喝完了(变成状态 B),又接了一杯水放在原处(变回状态 A)。你回来看到水还在,端起来就喝。虽然看起来还是水,但杯子可能被污染了,或者水的温度变了。你以为没变,实际上已经'偷梁换柱'了。

ABA 问题对于一些纯数值修改中通常无害,但在涉及指针和内存管理的动态数据结构中是致命的。

假设我们有一个无锁栈,栈顶指针为 Top。栈内元素:Top -> Node1 -> Node2。

故障出现过程:

  1. 线程 1 准备出栈(Pop):读取 Top 为 Node1。读取 Node1->next 为 Node2。(此时线程 1 被操作系统调度挂起)
  2. 线程 2 疯狂操作:
    • Pop:将 Node1 弹出,此时 Top 变为 Node2。
    • Pop:将 Node2 弹出,此时 Top 变为 nullptr。
    • Push:线程 2 创建了一个新节点,恰好操作系统的内存分配器将刚刚释放的 Node1 的内存地址重用给了这个新节点。
    • Push:将这个新节点(地址仍然是 Node1)压入栈。
    • 现状:Top 指向了新 Node1,但 Node1->next 不再指向 Node2(因为 Node2 已经被释放了)。
  3. 线程 1 恢复执行:
    • 执行 CAS 操作:compare_exchange(Top, expected=Node1, new=Node2)。
    • 判断:检查 Top 当前的值,发现地址确实是 Node1(尽管内容变了,但地址没变)。
    • 交换:CAS 成功!线程 1 将 Top 修改为 Node2。

后果:Top 现在指向了 Node2。但是 Node2 早在步骤 2 中被线程 2 释放回内存池了!任何后续访问 Top 的操作都会访问野指针,导致程序崩溃或数据错乱。

从底层原因看,首先是操作系统或内存分配器倾向于复用刚刚释放的内存块(热缓存),而且 CAS 是浅层比较,它只管二进制数据是否相同。

对于以上 ABA 问题,我们可以采取版本号、std::atomic<std::shared_ptr>、Hazard Pointers 来解决。在此,我们介绍下利用版本号进行控制。

首先定义一个结构体,它将指针和版本号捆绑在一起:

// 假设 Node 是栈中的节点结构
struct Node {
    int data;
    Node* next;
};

// 复合状态结构体
struct PointerWithVersion {
    Node* pointer; // 栈顶指针 (Top)
    unsigned int version; // 版本计数器
};

// 栈顶指针现在是这个复合结构的原子变量
std::atomic<PointerWithVersion> top;

然后实现正确的 Pop 操作:

// 模拟无锁栈的 Pop 操作
Node* pop() {
    // 1. 原子地读取当前的 (指针 A, 版本 v1) 状态
    PointerWithVersion expected = top.load();
    
    // 2. 构造线程 1 想要的新状态:指针变为 A->next,版本号递增
    PointerWithVersion desired;
    if (expected.pointer == nullptr) {
        return nullptr;
    }
    
    // 线程 1 进入 CAS 循环
    do {
        // 确保在循环内读取最新状态,因为 expected 可能会在 CAS 失败时被更新
        Node* current_node = expected.pointer;
        
        // 构造新状态:指向下一个节点,版本号递增
        desired.pointer = current_node->next;
        desired.version = expected.version + 1;
        
        // 关键的原子操作
        // 如果 top 仍等于 expected,则 top = desired,返回 true
        // 如果 top 不等于 expected,则 top 被更新为 top 的最新值,返回 false,继续循环
    } while (!top.compare_exchange_weak(expected, desired));
    
    // CAS 成功,current_node 已被弹出
    return expected.pointer;
}

现在我们来分析一下 ABA 问题的解决过程,利用表格进行对照:

状态无版本号 (单指针)有版本号 (复合结构)
初始状态Top = Node1Top = (Node1, 1)
线程 1 快照expected = Node1expected = (Node1, 1)
线程 2 操作导致 Top 变为 Node1 (地址复用)导致 Top 变为 (Node1, 3) (地址复用,版本递增)
线程 1 恢复执行,尝试 CASCAS(Top, Node1, Node2)CAS(Top, (Node1, 1), (Node2, 2))
CAS 比较比较 Node1 和 Node1。值相等,CAS 成功。比较 (Node1, 3) 和 (Node1, 1)。值不等,CAS 失败。
结果Top 变为野指针 Node2。expected 被更新为 (Node1, 3)。线程 1 发现冲突,重新循环,基于最新的状态继续尝试操作。

版本号将 CAS 的比较粒度从一个地址提升到了地址与历史状态的组合。即使指针 A 变回了 A,但版本号 v1 已经变成了 v3。线程 1 的预期值 A_{v1} 永远不会等于内存中的最新值 A_{v3},因此 CAS 会失败,线程被迫读取最新的 (Node1, 3) 状态并重试,从而阻止了它将栈顶链接到早已被释放的旧节点上。

5. 自旋锁
5.1 原子'交换'

使用 std::atomic<bool> 实现锁,并不是简单的 if 判断,而是利用了 原子的'读 - 改 - 写' 能力。

加锁逻辑:

线程尝试将布尔值设置为 true(表示'我占用了')。exchange(true) 函数会把变量设为 true,并返回旧值。如果返回 false:说明之前是解锁状态,我现在抢到了锁。成功!如果返回 true:说明之前已经是 true(被别人占用了)。失败,继续循环重试(自旋)。

解锁逻辑:

将布尔值设回 false(store(false))。

5.2 代码实现
#include <atomic>
#include <thread>

class SpinLock {
private:
    std::atomic<bool> flag = {false}; // false 代表未加锁,true 代表已加锁

public:
    void lock() {
        // exchange(true) 会把 flag 设为 true,并返回原来的值。
        // 只要返回 true,说明原来就被锁住了,我就在 while 里一直空转(Spin)。
        // 只有返回 false,说明原来没锁,我成功锁住了,跳出循环。
        // std::memory_order_acquire 保证之后的代码不会被重排到这一行之前
        while (flag.exchange(true, std::memory_order_acquire)) {
        }
    }

    void unlock() {
        // 解锁很简单,直接设为 false。
        // std::memory_order_release 保证之前的代码不会被重排到这一行之后
        flag.store(false, std::memory_order_release);
    }
};

可以利用该自旋锁代替 std::mutex,进行性能对比:

特性std::mutex (互斥锁)atomic 自旋锁 (Spinlock)
等待方式睡眠:如果拿不到锁,操作系统会把线程挂起(放入等待队列)忙等:如果拿不到锁,CPU 一直空转循环,不放弃时间片
上下文切换有:线程挂起/唤醒需要内核介入,开销较大(几微秒级别)无:完全在用户态执行,没有系统调用,开销极小(纳秒级别)
CPU 占用低(等待时不耗 CPU)高(等待时 CPU 满负荷空转)
适用场景临界区代码较长,或有 I/O 操作临界区短,且竞争不激烈

在 C++ 中,最标准的自旋锁其实推荐使用 std::atomic_flag,而 std::atomic<bool> 上述实现逻辑是为了更好的理解自旋锁的形成。它的接口非常简单,专门为自旋锁设计:

test_and_set(): 相当于 exchange(true)。 clear(): 相当于 store(false)。

目录

  1. 1. CAS 的核心逻辑
  2. 2. C++ 中的 CAS 函数
  3. A. compareexchangeweak(弱 CAS)
  4. B. compareexchangestrong(强 CAS)
  5. 3. 代码示例:标准的 CAS 循环
  6. 4. ABA 问题
  7. 5. 自旋锁
  8. 5.1 原子“交换”
  9. 5.2 代码实现
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • ToDesk、顺网云与海马云:DeepSeek 大模型云端部署实测对比
  • C++ STL 容器常用函数与实战技巧
  • OpenClaw 对接 Stable Diffusion 教程:免费畅享 AI 绘画
  • Ubuntu 系统下 Python 连接 KingbaseES 数据库实现增删改查
  • Rust、Go、Java、Python、Node.js 内存管理深度对比
  • Python 2026 发展局势:AI 时代的通用基础设施语言
  • AI 提示词模板:3 分钟生成 3000 字电商产品详情页文案
  • Fish Speech-1.5 语音风格控制:通过描述词定制音色与语调
  • OpenClaw 对接飞书机器人:插件安装与回调配置踩坑指南
  • ThreadLocal 原理、使用场景及内存泄漏问题解析
  • 8 篇必读的大模型论文精选
  • LLaMA 大模型本地化稳定部署指南:基于 Ollama 与 NextChat
  • 火影忍者主题静态网页设计与实现指南
  • Linux 权限详解:从文件系统到用户组策略
  • Seedream 4.0 深度测评:AI 图像生成与企业级应用
  • AI 工作流实践:构建日报生成器
  • DeepSeek-OCR-WEBUI 开源实践与部署指南
  • 配置钉钉 OpenClaw 机器人调用 OpenMetadata
  • BFS 解决 FloodFill 算法:从图像渲染到岛屿问题实战
  • 如何成为懂 AI 的产品经理

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online