跳到主要内容C++ 原子操作 compare_exchange_weak 详解 | 极客日志C++算法
C++ 原子操作 compare_exchange_weak 详解
详细解析了 C++ std::atomic::compare_exchange_weak 原子操作,涵盖函数定义、CAS 执行逻辑、伪失败特性、内存序规则及与强版本的区别。重点介绍了其在无锁数据结构(如无锁链表)中的应用,以及必须配合循环使用的约束。此外,文章还深入探讨了 volatile 与 atomic 的结合使用场景,特别是嵌入式硬件寄存器操作中的必要性,以及 volatile 修饰函数的语法规则、底层作用及典型应用场景,旨在帮助开发者掌握高性能无锁同步的关键技术。
信号故障2 浏览 C++ std::atomic::compare_exchange_weak 全解析
std::atomic::compare_exchange_weak 是 C++ <atomic> 库中核心的原子比较 - 交换(CAS,Compare-And-Swap)原语,是实现无锁同步、无锁数据结构的底层基础。通过原子化完成'比较 - 替换'逻辑,支持轻量级的伪失败特性,在循环场景中兼具极致性能和线程安全性。
一、函数核心定义与重载形式
该函数提供 4 个重载版本,分为单内存序版和双内存序版,同时支持 volatile 修饰的原子对象,所有版本均为 保证:
noexcept
1. 单内存序版(重载 1)
bool compare_exchange_weak(T& expected, T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak(T& expected, T val, memory_order sync = memory_order_seq_cst) noexcept;
- 单个
sync 参数指定整个操作的内存序,无论比较成功/失败均使用该规则;
- 默认内存序为
memory_order_seq_cst(顺序一致性)。
2. 双内存序版(重载 2)
bool compare_exchange_weak(T& expected, T val, memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_weak(T& expected, T val, memory_order success, memory_order failure) noexcept;
- 精细化区分内存序:
success 用于比较成功时的内存约束,failure 用于比较失败时的内存约束;
- 有严格规则:
failure 内存序不能强于 success,且不能是 memory_order_release 或 memory_order_acq_rel。
- 模板参数
T:原子类型封装的底层基础类型;
- 返回值:
bool 类型,比较成功返回 true,否则返回 false;
- 核心入参:
expected 为引用传递,会被函数修改。
二、核心原子执行逻辑(CAS 核心)
compare_exchange_weak 的核心是原子化完成'比较 - 替换 - 更新'三步操作,整个过程不可被任何线程中断:
- 原子读取:读取原子对象内部的当前真实值(记为
current);
- 物理比较:直接比较
current 与入参 expected 的原始内存二进制内容;
- 分支执行:
- ✅ 比较成功:将原子对象的内部值原子替换为入参
val,expected 保持不变,函数返回 true;
- ❌ 比较失败:不修改原子对象的内部值,而是将
current 覆盖写入入参 expected,函数返回 false。
关键注意点:物理比较 vs 逻辑比较
该函数的比较是逐字节的内存内容对比,而非通过 operator== 进行逻辑判断。即使两个值通过 operator== 判断为相等,若底层类型存在填充位、陷阱值,也可能导致比较失败。但这种情况在循环中会快速收敛。
三、弱版本核心特性:伪失败(Spurious Failure)
这是 compare_exchange_weak 与 compare_exchange_strong 最本质的区别,也是其性能优势的来源。
1. 伪失败的定义
compare_exchange_weak 允许在 expected 与原子对象真实值完全相等时,依然无原因地返回 false。
2. 伪失败的成因与特点
- 成因:与硬件架构相关,是硬件层面的设计取舍;
- 关键特点:伪失败时,函数返回
false,且不会修改入参 expected;
- 概率:发生概率极低,在循环场景中几乎可以忽略。
3. 伪失败的影响与使用约束
- 必须配合循环使用:由于伪失败的存在,非循环场景绝对不能使用
compare_exchange_weak;
- 性能优势:弱版本的 CAS 指令比强版本更轻量级,在循环场景中整体性能显著更高。
四、内存序参数详解(单/双版本规则)
1. 单内存序版(sync 参数)
- 规则:无论比较成功还是失败,全程使用指定的
sync 内存序;
- 适用场景:对性能要求不高,希望简化代码的普通循环场景。
2. 双内存序版(success/failure 参数)
- 成功内存序(
success):比较成功时的内存序,此时原子对象被写入;
- 失败内存序(
failure):比较失败时的内存序,此时仅读取原子对象;
- 强制规则:
failure 的内存序不能强于 success;
- 高性能推荐组合:
success = memory_order_acq_rel,failure = memory_order_relaxed。
3. 常用内存序说明(快速参考)
| 内存序常量 | 核心作用 |
|---|
| memory_order_relaxed | 松散内存序,仅保证操作本身原子性,性能最高 |
| memory_order_acquire | 获取型,读操作后后续指令不能重排到该操作之前 |
| memory_order_release | 释放型,写操作前指令不能重排到该操作之后 |
| memory_order_acq_rel | 读写型,适合'读 - 改 - 写'原子操作 |
| memory_order_seq_cst | 顺序一致性,最强约束,性能最低 |
五、核心适用场景
compare_exchange_weak 是无锁编程的基石,核心适用于需要循环重试的 CAS 场景:
- 无锁共享计数器:实现多线程安全的自增/自减;
- 无锁数据结构:实现无锁链表、无锁队列、无锁栈等;
- 轻量级无锁同步:替代简单互斥锁实现线程间的状态切换;
- 高并发底层框架:网络服务器、分布式存储等核心同步原语。
六、官方示例深度解析(无锁链表头插)
官方示例是 compare_exchange_weak 的经典应用——实现线程安全的无锁单向链表头插操作。
示例完整代码
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
struct Node {
int value;
Node* next;
};
std::atomic<Node*> list_head(nullptr);
void append(int val) {
Node* oldHead = list_head.load();
Node* newNode = new Node {val, oldHead};
while (!list_head.compare_exchange_weak(oldHead, newNode)) {
newNode->next = oldHead;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.push_back(std::thread(append, i));
}
for (auto& th : threads) th.join();
for (Node* it = list_head; it != nullptr; it = it->next) {
std::cout << ' ' << it->value;
}
std::cout << '\n';
Node* it;
while ((it = list_head)) {
list_head = it->next;
delete it;
}
return 0;
}
核心逻辑拆解
- 初始读取:每个线程先读取当前的原子头指针
list_head 到 oldHead,并创建新节点;
- 第一次 CAS 尝试:调用
compare_exchange_weak(oldHead, newNode),尝试将原子头指针从 oldHead 替换为新节点;
- 若成功:插入完成;
- 若失败:其他线程已修改了
list_head(或发生伪失败),oldHead 已被更新为最新真实值;
- 更新并重试:将新节点的
next 重新指向更新后的 oldHead,再次执行 CAS;
- 循环直至成功:确保节点正确插入,无丢失。
七、与 compare_exchange_strong 的核心区别
| 特性 | compare_exchange_weak | compare_exchange_strong |
|---|
| 伪失败 | 允许(核心特性) | 禁止(绝对可靠) |
| 性能 | 更高(轻量级指令) | 稍低(重量级指令) |
| 循环依赖性 | 必须配合循环使用 | 可单独使用 |
- 循环场景:优先使用 weak 版—— 性能优势显著;
- 非循环场景:必须使用 strong 版—— 单次执行需要绝对可靠的结果。
- 一句话总结:循环用 weak,非循环用 strong。
八、使用注意事项(避坑关键)
- 必须循环使用:弱版本的核心约束,非循环场景使用会因伪失败导致逻辑错误;
- expected 是引用:切勿将其声明为常量,函数需要在失败时修改它;
- 物理比较的坑:若底层类型 T 有填充位、陷阱值,可能出现'逻辑相等但物理比较失败'的情况;
- 内存序的选择:普通场景用默认的
memory_order_seq_cst,高性能场景再使用双内存序版;
- 无锁并非万能:若 CAS 的重试次数过多,会导致 CPU 空转,此时不如使用互斥锁。
九、核心总结
std::atomic::compare_exchange_weak 是原子 CAS 操作的弱版本,核心是原子化完成'比较 - 替换 - 更新';
- 核心特性是允许伪失败,牺牲单次可靠性换取极致性能,必须配合循环使用;
- 提供单/双内存序重载,双内存序版可精细化控制成功/失败的内存约束;
- 是无锁编程的基石,核心适用于无锁数据结构、无锁计数器等循环重试场景;
- 与强版本的核心区别是伪失败,循环用 weak,非循环用 strong 是开发的核心选择原则。
volatile 叠加 atomic
一、compare_exchange_weak 带 volatile 重载版本的核心作用
std::atomic::compare_exchange_weak 提供带 volatile 限定的成员函数重载,核心作用是支持对「被 volatile 修饰的 std::atomic<T> 原子对象」进行原子 CAS 操作,本质是为 volatile std::atomic<T> 类型的对象提供匹配的成员函数调用接口。
二、std::atomic<T> 再用 volatile 修饰的含义
std::atomic<T> 本身天然具备「禁止编译器内存优化、直接访问物理内存」的特性,此时再用 volatile 修饰,是为原子对象叠加一层「硬件/外部异步操作感知」的语义,核心适用场景是:原子对象映射到「内存映射的硬件寄存器地址」。
关键补充:std::atomic 与 volatile 的能力边界
std::atomic<T>:核心保证操作的原子性,同时天然禁止编译器对其做寄存器缓存等优化;
volatile:核心作用是抑制编译器优化,强制直接访问物理内存,但不保证操作原子性。
因此,volatile std::atomic<T> 是双重约束的原子类型,具备两大核心能力:
- 继承
std::atomic<T> 的原子操作能力;
- 叠加
volatile 的硬件异步感知能力。
三、volatile std::atomic<T> 的典型适用场景
仅在嵌入式开发中,原子对象需要直接映射到硬件寄存器时使用。硬件寄存器的特点是:
- 被映射到固定的物理内存地址,其值可能被硬件异步修改;
- 对其的读写操作有硬件副作用,不允许编译器优化。
#define PERIPH_STATUS (*(volatile std::atomic<uint32_t>*)0x40001000)
void update_status() {
uint32_t expected = PERIPH_STATUS.load();
while (!PERIPH_STATUS.compare_exchange_weak(expected, expected | 0x01)) {
}
}
四、普通多线程场景的注意事项
纯软件多线程场景中,绝对不要用 volatile 修饰 std::atomic<T>,因为 std::atomic<T> 天然具备 volatile 的编译器优化抑制能力,叠加 volatile 无任何额外收益。
五、核心总结
- 带
volatile 的 compare_exchange_weak 重载:是语法兼容接口,仅用于支持对 volatile std::atomic<T> 类型对象的 CAS 调用;
std::atomic<T> 加 volatile 修饰:得到 volatile std::atomic<T>,是原子性 + 硬件异步感知的双重约束类型;
- 适用场景:仅在嵌入式开发中,原子对象映射到硬件寄存器时使用,纯软件多线程场景无需加
volatile。
C++ 中 volatile 修饰函数的全面详解
volatile 修饰函数是 C++ 中类型限定符的重要应用形式,核心作用是为被 volatile 修饰的对象提供专属的成员函数调用接口,是语法层面的严格约束。
一、volatile 修饰函数的核心定义与语法形式
volatile 修饰函数仅适用于类/结构体的成员函数,是对成员函数的 this 指针进行限定,语法上写在函数参数列表后。
基础语法形式
class MyClass {
public:
void func() volatile;
int getVal() volatile noexcept;
};
与 const 结合的重载形式(最常用)
volatile 可与 const 共同修饰成员函数,形成函数重载:
class MyClass {
public:
void show();
void show() const;
void show() volatile;
void show() const volatile;
};
核心本质:volatile 修饰成员函数 → 表示该函数可以被 volatile 修饰的类对象调用。
二、volatile 修饰函数的核心语法规则(必守)
被 volatile 修饰的对象,只能调用其 volatile 限定的成员函数;非 volatile 对象,可调用任意限定(含 volatile)的成员函数。
反之则语法报错:非 volatile 成员函数,无法被 volatile 对象调用。
规则示例验证
class Test {
public:
void f1() {}
void f2() volatile {}
};
int main() {
Test obj1;
volatile Test obj2;
obj1.f1();
obj1.f2();
obj2.f1();
obj2.f2();
return 0;
}
三、volatile 修饰函数的底层核心作用(3 点)
除了语法层面的调用匹配,volatile 修饰成员函数后,会对函数的执行过程、内存访问、编译器优化施加严格限制:
作用 1:禁止编译器对函数内部的成员变量访问做优化
函数内部通过 this 指针访问的所有成员变量,编译器不会将其缓存到 CPU 寄存器,也不会省略重复的读写操作,每次访问都必须直接操作物理内存。
作用 2:禁止编译器对函数内部的指令做重排优化
编译器不会对 volatile 函数内部的内存读写指令进行重排,保证指令的执行顺序与源码顺序完全一致。
作用 3:限制函数内部对对象状态的修改权限
volatile 成员函数内部,无法修改对象的非 volatile 成员变量(除非成员变量被 mutable 修饰),也无法调用对象的非 volatile 成员函数。
四、volatile 函数的典型适用场景
volatile 修饰函数并非通用特性,而是为嵌入式开发、硬件交互、底层系统编程设计的,仅在对象本身被 volatile 修饰的场景下使用。
场景 1:嵌入式开发中,操作内存映射的硬件寄存器对象
嵌入式开发中,硬件寄存器通常会被映射到固定的物理内存地址,通常会封装为类对象,且该对象必须被 volatile 修饰。此时,为该类定义的所有成员函数都必须被 volatile 修饰。
场景示例:封装硬件 GPIO 寄存器类
class GPIOA {
public:
static constexpr uint32_t ODR_ADDR = 0x4001080C;
uint32_t& odr = *(reinterpret_cast<uint32_t*>(ODR_ADDR));
void ledOn() volatile {
odr |= (1 << 5);
}
void ledOff() volatile {
odr &= ~(1 << 5);
}
};
volatile GPIOA gpioa;
int main() {
while (1) {
gpioa.ledOn();
delay_ms(500);
gpioa.ledOff();
delay_ms(500);
}
return 0;
}
场景 2:为 volatile std::atomic<T> 原子对象提供成员函数调用接口
std::atomic<T> 作为标准库的原子类型,支持被 volatile 修饰,因此其所有成员函数都提供了对应的 volatile 重载版本,保证 volatile std::atomic<T> 对象能正常调用所有原子操作。
场景 3:底层系统编程中,操作被异步操作修改的全局对象
系统编程中,若某个全局类对象可能被内核中断、其他进程/线程异步修改,该对象会被 volatile 修饰,此时为该对象定义的操作函数也必须被 volatile 修饰。
五、volatile 与 const 共同修饰函数(const volatile)
volatile 可与 const 共同修饰成员函数,形成 const volatile 双重限定,这是工程中常见的形式,核心是同时满足 const 和 volatile 的语义约束。
1. 双重限定的核心语义
- 继承
const 的语义:函数不会修改类对象的非 mutable 成员变量(只读操作);
- 继承
volatile 的语义:函数可被 volatile 对象调用,且内部内存访问无优化、指令不重排。
2. 调用规则
const volatile 函数是权限最低、兼容性最广的成员函数,可被任意限定的对象调用。
3. 示例:硬件寄存器的 const volatile 只读函数
class UART {
public:
static constexpr uint32_t SR_ADDR = 0x40013800;
const uint32_t& sr = *(reinterpret_cast<uint32_t*>(SR_ADDR));
bool isRecvReady() const volatile {
return (sr & (1 << 5)) != 0;
}
};
volatile UART uart1;
int main() {
while (!uart1.isRecvReady()) {
}
return 0;
}
六、volatile 函数的使用注意事项(避坑关键)
- 非必要不使用:仅在对象被
volatile 修饰时定义,纯软件编程中属于画蛇添足;
- 全局函数/静态成员函数不能被
volatile 修饰:C++ 语法规定,volatile 限定符仅适用于非静态成员函数;
- volatile 函数内部无法调用非 volatile 成员函数:因为
this 指针被限定为 volatile T*,需通过 const_cast 强制转换(谨慎使用);
- 不要混淆概念:
volatile 修饰函数、参数、返回值是完全不同的概念;
- 纯软件多线程场景,无需使用 volatile 函数:普通多线程中,对象无需被
volatile 修饰。
七、核心总结(精华提炼)
- 适用范围:
volatile 仅能修饰类的非静态成员函数,本质是对函数的 this 指针做 volatile 限定;
- 核心语法作用:为被
volatile 修饰的类对象提供调用接口,遵循「对象限定 ≤ 函数限定」规则;
- 底层核心作用:① 禁止函数内部成员变量的编译器优化;② 禁止函数内部内存指令重排;③ 限制函数内部对对象状态的修改;
- 与 const 结合:
const volatile 双重限定函数,同时满足'只读'和'无优化',是兼容性最广的形式;
- 典型场景:① 嵌入式开发中操作内存映射的硬件寄存器类对象;② 标准库为
volatile std::atomic<T> 提供的成员函数重载;③ 底层系统编程中操作被异步修改的全局对象;
- 使用原则:对象被
volatile 修饰时,才需要定义对应的 volatile 成员函数,纯软件编程中完全无需使用。
简单来说:volatile 修饰函数的唯一目的,就是让 volatile 的类对象能正常调用成员函数,同时保证函数内部对对象的访问符合 volatile 的'易变、无优化'语义。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,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
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online