c++中的CAS是什么

在 C++ 中,CASCompare-And-Swap(比较并交换)的缩写,是一种重要的原子操作,常用于实现无锁并发编程。

1. 基本含义

CAS 操作会原子地执行以下逻辑:

复制代码
if (当前值 == 期望值) {
    当前值 = 新值;
    return true;
} else {
    期望值 = 当前值;
    return false;
}

即:它先比较某个内存位置的当前值与给定的期望值,如果相等,则将该位置更新为新值;否则不更新,并将期望值更新为当前值。整个过程在硬件层面保证原子性,不会被其他线程打断。

2. 在 C++ 中的实现

C++11 开始,标准库提供了 <atomic> 头文件,其中的 std::atomic 模板类封装了 CAS 操作,主要通过两个成员函数:

  • compare_exchange_weak(T& expected, T desired)

  • compare_exchange_strong(T& expected, T desired)

区别

  • weak 版本在某些平台上可能会出现"伪失败"(即使值相等也可能返回 false),通常用在循环中。

  • strong 版本保证只有值不相等时才返回 false,语义更强但可能稍慢。

使用示例:

update() 函数想要原子地将 counter 的值增加 1,而且这个过程是线程安全的。

复制代码
#include <atomic>
#include <iostream>

std::atomic<int> counter(0);

void update() {
    //声明了一个名为 counter的变量,它的类型是 std::atomic<int> 
  • 这里的 int 表示这个原子变量内部存储的是一个整数。
  • counter(0) 表示初始值为 0
  • std::atomic 是 C++ 标准库提供的原子类型模板。

  • 普通的 int 变量在多线程环境下,如果同时读写,会发生数据竞争(data race),导致未定义行为。

  • std::atomic<int> 保证了对它的所有操作(读、写、修改等)都是原子操作------即这些操作要么一次性完成,要么完全不发生,不会被其他线程打断。

  • 原子变量的操作会通过特殊的 CPU 指令实现,不需要使用互斥锁(mutex),因此效率更高。

复制代码
    int expected = counter.load();
  • load()std::atomic 的一个成员函数,用于原子地读取当前原子变量中存储的值。

  • 这里把 counter 的当前值读取出来,赋值给普通局部变量 expected

  • 因为 load() 是原子的,所以读取时不会发生数据竞争,读到的一定是某个时刻的完整值。

此时,expectedcounter旧值 ,我们打算把它加 1,得到新值 desired

复制代码
    int desired = expected + 1;
    // 尝试将 counter 从 expected 更新为 desired
//这是整个代码的灵魂,它执行了一个 CAS(Compare-And-Swap) 操作。
    while (!counter.compare_exchange_weak(expected, desired)) {
  • 参数 expected:是一个引用。调用时,我们传入之前读取到的旧值。

  • 参数 desired:我们想要设置的新值。

  • 返回值true 表示成功更新了原子变量;false 表示更新失败。

执行逻辑(原子完成)

  • 比较 :将原子变量 counter 的当前值与 expected 进行比较。

  • 如果相等 :说明从我们读取 expected 到现在,counter 没有被其他线程改变过。于是将 counter 更新为 desired,并返回 true

  • 如果不相等 :说明在此期间,其他线程已经修改了 counter。此时 CAS 操作不会更新 counter,而是把 counter 的当前值写入 expected(通过引用修改),并返回 false

为什么需要循环?

因为 CAS 可能失败(当其他线程同时修改了 counter 时),失败后我们需要重试

  • 失败后,expected 已经被更新为 counter 的最新值。

  • 我们基于这个最新的 expected 重新计算 desired(即 expected + 1)。

  • 再次尝试 CAS,直到成功为止。

复制代码
        // 如果失败,expected 已被更新为当前值,重新计算 desired
        desired = expected + 1;
    }
}

这段代码实现了一个线程安全的计数器增加操作,避免了使用互斥锁。

3. 主要用途

  • 实现无锁数据结构(如无锁栈、队列)。

  • 实现自旋锁等轻量级同步机制。

  • 原子地更新共享变量,避免数据竞争。

4. 注意事项

  • ABA 问题 :如果某个值从 A 变为 B 再变回 A,CAS 可能会误判为没有变化而成功更新。解决方法通常是使用带版本号的原子类型(如 std::atomic<int> 结合一个递增的标记)或使用带标签的指针。

  • 性能:CAS 通常比互斥锁轻量,但在高冲突下可能导致大量重试。

5. 总结

CAS 是 C++ 并发编程的基石之一,通过 std::atomic 提供的 compare_exchange 系列函数,开发者可以构建高效、无锁的并发算法。

复制代码
#include <atomic>
#include <iostream>

std::atomic<int> counter(0);

void update() {
    int expected = counter.load();
    int desired = expected + 1;
    // 尝试将 counter 从 expected 更新为 desired
    while (!counter.compare_exchange_weak(expected, desired)) {
        // 如果失败,expected 已被更新为当前值,重新计算 desired

        //如果失败(说明 counter 被其他线程改了),expected 会被自动更新为最新的 counter 值,然后回到第 2 步,重新计算 desired 并再次尝试。
        desired = expected + 1;
    }
}

明白了,这句的意思就是while (!counter.compare_exchange_weak(expected, desired))
如果counter和expected,一样,desired就是对的,就是我们要的,如果不一样,expected 会被自动更新为最新的 counter 值,然后再指向desired = expected + 1

对于这段代码的详细解读

1. std::atomic<int> counter(0); 是什么?

这行代码声明了一个名为 counter 的变量,它的类型是 std::atomic<int>

  • std::atomic 是 C++ 标准库提供的原子类型模板。

  • 这里的 int 表示这个原子变量内部存储的是一个整数。

  • counter(0) 表示初始值为 0

关键点

  • 普通的 int 变量在多线程环境下,如果同时读写,会发生数据竞争(data race),导致未定义行为。

  • std::atomic<int> 保证了对它的所有操作(读、写、修改等)都是原子操作------即这些操作要么一次性完成,要么完全不发生,不会被其他线程打断。

  • 原子变量的操作会通过特殊的 CPU 指令实现,不需要使用互斥锁(mutex),因此效率更高。


2. counter.load() 的作用

复制代码
int expected = counter.load();
  • load()std::atomic 的一个成员函数,用于原子地读取当前原子变量中存储的值。

  • 这里把 counter 的当前值读取出来,赋值给普通局部变量 expected

  • 因为 load() 是原子的,所以读取时不会发生数据竞争,读到的一定是某个时刻的完整值。

此时,expectedcounter旧值 ,我们打算把它加 1,得到新值 desired


3. 核心:compare_exchange_weak(expected, desired)

复制代码
while (!counter.compare_exchange_weak(expected, desired)) {
    desired = expected + 1;
}

这是整个代码的灵魂,它执行了一个 CAS(Compare-And-Swap) 操作。

函数签名回顾

复制代码
bool compare_exchange_weak(T& expected, T desired) noexcept;
  • 参数 expected:是一个引用。调用时,我们传入之前读取到的旧值。

  • 参数 desired:我们想要设置的新值。

  • 返回值true 表示成功更新了原子变量;false 表示更新失败。

执行逻辑(原子完成)

  1. 比较 :将原子变量 counter 的当前值与 expected 进行比较。

  2. 如果相等 :说明从我们读取 expected 到现在,counter 没有被其他线程改变过。于是将 counter 更新为 desired,并返回 true

  3. 如果不相等 :说明在此期间,其他线程已经修改了 counter。此时 CAS 操作不会更新 counter,而是把 counter 的当前值写入 expected(通过引用修改),并返回 false

为什么需要循环?

因为 CAS 可能失败(当其他线程同时修改了 counter 时),失败后我们需要重试

  • 失败后,expected 已经被更新为 counter 的最新值。

  • 我们基于这个最新的 expected 重新计算 desired(即 expected + 1)。

  • 再次尝试 CAS,直到成功为止。

weakstrong 版本的区别

  • compare_exchange_weak 在某些硬件平台上可能出现"伪失败"(即使值相等也可能返回 false),但这在循环中是可以接受的,因为循环会重试。

  • compare_exchange_strong 保证只有值不等时才返回 false,但某些平台上可能稍慢。

    在循环中使用 weak 更高效,因为循环会处理偶尔的伪失败。


4. 整个 update() 函数在做什么?

update() 函数想要原子地将 counter 的值增加 1,而且这个过程是线程安全的。

用自然语言描述:

  1. 先读取 counter 的当前值,存入 expected

  2. 计算新值 desired = expected + 1

  3. 调用 CAS 尝试更新:

    • 如果成功,函数结束。

    • 如果失败(说明 counter 被其他线程改了),expected 会被自动更新为最新的 counter 值,然后回到第 2 步,重新计算 desired 并再次尝试。

最终,无论有多少线程同时调用 update()counter 的值都会正确地增加,不会出现数据竞争导致的错误结果(例如少加或多加)。


5. 对比:如果不使用原子操作

假设我们写一个普通函数:

复制代码
void bad_update() {
    counter++;   // counter 是普通 int
}

如果多个线程同时执行 counter++,可能会发生以下情况:

  • 线程 A 读取 counter=10,准备加 1 写回 11;

  • 线程 B 同时读取 counter=10,也准备加 1 写回 11;

  • 最终 counter 可能只变成 11,而不是期望的 12,因为两次递增互相覆盖了。

使用原子 CAS 循环可以避免这种问题,保证每次递增都是基于最新的值进行的。


6. 扩展:C++ 中更简单的原子递增

其实对于递增这种常见操作,std::atomic 已经提供了直接的方法:

复制代码
counter.fetch_add(1);   // 原子地增加 1,返回旧值
// 或
counter++;   // 也支持后置/前置自增运算符(原子操作)

但理解 CAS 是实现无锁数据结构的基础,所以学习这段代码对深入并发编程很有帮助。


总结

  • counter 是一个原子整数,保证所有操作线程安全。

  • .load() 是原子地读取当前值。

  • compare_exchange_weak 是 CAS 操作:比较并可能交换,失败时自动更新 expected

  • 循环是为了在并发修改时重试,直到成功更新。

现在再回头看这段代码,是不是清晰多了?如果还有疑问,欢迎继续追问!

相关推荐
河西石头2 小时前
powerconfig告别繁琐配置读写---为C#提供了一个快捷的读写配置文件的API
开发语言·c#·高效读写配置文件·c#配置文件·xml读写
IOT-Power2 小时前
QT 事件驱动架构
开发语言·qt·架构
2401_889884662 小时前
模板代码模块化设计
开发语言·c++·算法
java1234_小锋2 小时前
Java高频面试题:RabbitMQ中有哪几种交换机类型?
java·rabbitmq·java-rabbitmq
Trouvaille ~2 小时前
【贪心算法】专题(五):逆向思维与区间重叠的极致拉扯
c++·算法·leetcode·青少年编程·面试·贪心算法·蓝桥杯
翘着二郎腿的程序猿2 小时前
SpringBoot集成@Slf4j注解:优雅输出日志,告别手动new Logger
java·spring boot·intellij-idea
wyt5314292 小时前
新手如何快速搭建一个Springboot项目
java·spring boot·后端
jinanmichael2 小时前
【SpringBoot】日志文件
java·spring boot·spring
qq_246100052 小时前
CSDN risk probe 1773588273
开发语言·javascript·ecmascript