在 C++ 中,CAS 是 Compare-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()是原子的,所以读取时不会发生数据竞争,读到的一定是某个时刻的完整值。
此时,expected 是 counter 的旧值 ,我们打算把它加 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()是原子的,所以读取时不会发生数据竞争,读到的一定是某个时刻的完整值。
此时,expected 是 counter 的旧值 ,我们打算把它加 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表示更新失败。
执行逻辑(原子完成)
-
比较 :将原子变量
counter的当前值与expected进行比较。 -
如果相等 :说明从我们读取
expected到现在,counter没有被其他线程改变过。于是将counter更新为desired,并返回true。 -
如果不相等 :说明在此期间,其他线程已经修改了
counter。此时 CAS 操作不会更新counter,而是把counter的当前值写入expected(通过引用修改),并返回false。
为什么需要循环?
因为 CAS 可能失败(当其他线程同时修改了 counter 时),失败后我们需要重试:
-
失败后,
expected已经被更新为counter的最新值。 -
我们基于这个最新的
expected重新计算desired(即expected + 1)。 -
再次尝试 CAS,直到成功为止。
weak 与 strong 版本的区别
-
compare_exchange_weak在某些硬件平台上可能出现"伪失败"(即使值相等也可能返回false),但这在循环中是可以接受的,因为循环会重试。 -
compare_exchange_strong保证只有值不等时才返回false,但某些平台上可能稍慢。在循环中使用
weak更高效,因为循环会处理偶尔的伪失败。
4. 整个 update() 函数在做什么?
update() 函数想要原子地将 counter 的值增加 1,而且这个过程是线程安全的。
用自然语言描述:
-
先读取
counter的当前值,存入expected。 -
计算新值
desired = expected + 1。 -
调用 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。 -
循环是为了在并发修改时重试,直到成功更新。
现在再回头看这段代码,是不是清晰多了?如果还有疑问,欢迎继续追问!