文章目录
C++ 并发编程入门:std::atomic 原子变量详解
**多线程环境下,如何安全高效地操作共享数据? **
假设你需要实现一个计数器,两个线程同时对其执行 10 万次递增操作,最终结果会是多少?
一、为什么需要原子变量?
1.1 经典问题:多线程计数器
include <iostream>
include <thread>
int counter = 0; // 普通变量
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 非原子操作
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join(); t2.join();
std::cout << "Final counter: " << counter; // 输出可能是 120345(错误结果)
}
问题根源:counter++
是非原子操作,实际分为三步:
- 从内存读取值到寄存器
- 寄存器加 1
- 写回内存
两个线程可能同时读取旧值,导致最终结果小于 200000。
二、std::atomic 基础用法
2.1 修正计数器问题
include <atomic>
std::atomic<int> counter{0}; // 声明原子变量
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 原子操作
}
// 输出一定是 200000
2.2 核心特性
- 原子性:操作不可分割(如
++
,load
,store
)。 - 内存顺序控制:默认保证顺序一致性(
memory_order_seq_cst
)。 - 无锁设计:底层使用 CPU 原子指令(如 x86 的 LOCK 前缀)。
三、原子操作类型与性能
3.1 支持的原子操作
操作类型 | 示例 | 说明 |
---|---|---|
读取 | int val = counter.load(); | 原子读取当前值 |
写入 | counter.store(42); | 原子写入新值 |
交换 | int old = counter.exchange(5); | 原子替换为新值 |
比较并交换 | bool ok = counter.compare_exchange_strong(old, new); | CAS 操作 |
3.2 性能对比(示例:1000 万次操作)
方法 | 时间(ms) | 说明 |
---|---|---|
普通变量 + 互斥锁 | 120 | 需要系统调用,上下文切换 |
原子变量 | 25 | 无锁,CPU 指令级原子性 |
普通变量(单线程) | 8 | 无竞争,最快但线程不安全 |
四、内存顺序与安全性
4.1 内存顺序选项
内存顺序 | 说明 |
---|---|
memory_order_relaxed | 只保证原子性,无顺序约束(最快) |
memory_order_acquire | 当前操作的读必须在后续操作前完成 |
memory_order_release | 当前操作的写必须在后续操作前完成 |
memory_order_seq_cst | 默认选项,严格顺序一致性(最安全) |
4.2 安全示例:原子标志位
std::atomic<bool> flag{false};
int data = 0;
// 线程1:写入数据后设置标志
void producer() {
data = 42; // 非原子操作
flag.store(true, std::memory_order_release); // 保证data写入在flag之前
}
// 线程2:读取标志后读取数据
void consumer() {
while (!flag.load(std::memory_order_acquire)); // 保证data读取在flag之后
std::cout << data; // 一定输出42
}
五、原子变量的常见陷阱
5.1 复合操作仍需锁
std::atomic<int> a{0}, b{0};
// 错误:a和b的更新不是原子性的
void unsafe_update() {
a++;
b = a; // 其他线程可能在a++后修改a,导致b != a
}
// 正确:使用锁保护复合操作
std::mutex mtx;
void safe_update() {
std::lock_guard<std::mutex> lock(mtx);
a++;
= a;
}
5.2 误用运算符重载
std::atomic<int> counter{0};
// 错误:counter = counter + 1 不是原子操作!
counter = counter + 1;
// 正确:使用 fetch_add
counter.fetch_add(1);
六、最佳实践
- 简单操作用原子变量:计数器、标志位、状态机。
- 复杂操作用互斥锁:涉及多个变量或需要事务性操作时。
- 优先使用默认内存顺序:除非明确需要优化性能。
- 避免过度依赖
relaxed
顺序:容易引入难以调试的并发 Bug。
七、总结
- 核心价值:
std::atomic
通过硬件级原子指令,实现无锁线程安全操作。 - 性能优势:比互斥锁更快,适合高频简单操作。
- 适用场景:单变量原子操作(如计数器、标志位)。
- 局限性:无法替代锁处理复杂逻辑。
记住:原子变量是并发编程的利器,但绝不是万能药!