C++ atomic库的基本用法
作者:m0_73457176
std::atomic 是 C++11 引入的用于多线程编程的核心工具,它通过硬件提供的原子操作,让我们能更高效、简洁地处理共享数据的并发访问,避免了传统锁带来的性能开销和死锁风险。
C++ 的 std::atomic 本质上是把“读-改-写”这类操作在硬件层面变成不可分割的整体,从而让多线程共享数据时无需加锁也能保证安全。它解决的是数据竞争问题,核心价值在于“轻量级同步”。
🧠 核心概念:什么是原子操作?
原子操作可以理解为不可分割的最小操作单元,它要么完全执行,要么完全不执行,不会在执行到一半时被其他线程中断。
考虑一个简单的 counter++ 操作,它在底层通常分为“读取、修改、写入”三步。在多线程环境下,如果不加保护,两个线程可能同时读取到相同的值,然后各自加1,最终导致结果错误(例如预期为2,结果却为1),这就是竞态条件 (Race Condition)。std::atomic 的作用就是保证这类复合操作作为一个整体原子性地完成。
🛠️ 基本用法:如何使用std::atomic?
要使用原子类型,需要包含 <atomic> 头文件。
1. 声明与初始化
你可以直接使用 std::atomic<T> 模板,也可以用标准库提供的别名:
#include <atomic>
std::atomic<int> counter(0); // 整型原子变量
std::atomic<bool> flag(false); // 布尔型原子变量
std::atomic_int score{100}; // 使用别名2. 核心成员函数
std::atomic 提供了一组成员函数来执行原子操作:
load(): 原子地读取值。store(val): 原子地将值设为val。exchange(val): 原子地将值设为val,并返回旧值。fetch_add(val)/fetch_sub(val): 原子地加/减val,并返回操作前的值。compare_exchange_strong(expected, desired): CAS (Compare-And-Swap) 操作。如果当前值等于expected,则将其设为desired并返回true;否则,将expected更新为当前值并返回false。
3. 便捷运算符
对于整型和指针特化,std::atomic 也重载了常见运算符,使代码更自然:
std::atomic<int> num(0); num++; // 等价于 num.fetch_add(1) num += 5; // 等价于 num.fetch_add(5) int val = num; // 等价于 num.load()
⚠️ 重要陷阱与注意事项
使用 std::atomic 时有几个关键点需要留意:
并非所有操作都是原子的:这是最常见的误区。复合表达式如 x = x * 2 不是原子操作。因为它本质上是“读取 x、计算 x*2、写入 x”三步,中间可能被中断。应尽量使用 fetch_add 等专用成员函数,或使用 compare_exchange 实现复杂的原子更新逻辑。
std::atomic 不总是无锁的 (Lock-Free):原子操作是否无锁取决于数据类型的大小和硬件架构。对于 int、long 等小型类型,通常是无锁的。但对于大型结构体,底层实现可能会退化为使用内部锁。你可以通过 is_lock_free() 方法在运行时检查。
std::atomic<int> a; std::cout << "Is lock free? " << a.is_lock_free() << std::endl; // 通常为 true
性能并非绝对最优:原子操作需要硬件进行缓存同步和内存屏障,本身有开销。虽然通常比互斥锁快,但在高竞争场景下,大量线程反复尝试CAS失败并重试,性能可能不如直接使用互斥锁。选择哪种同步机制需要根据具体场景权衡。
🔩 进阶话题
1. 内存顺序 (Memory Order)
这是 std::atomic 的高级特性,用于精确控制原子操作周围的非原子内存访问如何被排序和可见。默认是 memory_order_seq_cst(顺序一致性),它最严格也最安全,但可能限制编译器/硬件优化。
C++ 定义了 6 种内存顺序:
| 内存顺序 | 含义简述 |
|---|---|
memory_order_relaxed | 最宽松,只保证原子操作本身的原子性,不提供任何跨线程的同步或顺序保证。 |
memory_order_acquire | 用于读操作。保证此操作之后的所有读写不会被重排到该操作之前。 |
memory_order_release | 用于写操作。保证此操作之前的所有读写不会被重排到该操作之后。 |
memory_order_acq_rel | acquire + release,用于读-修改-写操作。 |
memory_order_seq_cst | 默认,最严格,保证所有线程看到的操作顺序一致。 |
memory_order_consume | 作用类似 acquire,但仅对数据依赖的操作有顺序保证,更弱也更难正确使用。 |
一个经典用法是实现自旋锁 (SpinLock):
class SpinLock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire)) {
// 自旋等待
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
};2. C++20 新特性:wait()与notify_*()
C++20 为 std::atomic 增加了类似条件变量的 wait()、notify_one() 和 notify_all() 方法,提供了一种无需互斥锁的高效阻塞等待机制。
wait(old_value): 如果原子变量的当前值等于old_value,则阻塞当前线程,直到被notify唤醒。notify_one()/notify_all(): 唤醒一个或所有在该原子变量上等待的线程。
这非常适合实现一个高效的事件标志。等待线程不再需要忙等(自旋) 消耗CPU,而是由操作系统挂起,直到被通知唤醒。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
#include <chrono>
// 全局原子变量
std::atomic<int> counter{0};
std::atomic<bool> ready{false};
// ---------- 示例 1:原子计数器 ----------
void increment_worker() {
for (int i = 0; i < 100000; ++i) {
// relaxed 只保证原子性,不需要额外的顺序约束即可保证累加正确
counter.fetch_add(1, std::memory_order_relaxed);
}
}
// ---------- 示例 2:acquire / release 同步 ----------
void waiter_thread() {
std::cout << "[Waiter] 等待信号...\n";
// 自旋等待:直到 ready 变为 true
while (!ready.load(std::memory_order_acquire)) {
std::this_thread::yield(); // 让出 CPU,避免把核心跑满
}
std::cout << "[Waiter] 信号已收到!\n";
}
void notifier_thread() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "[Notifier] 发出信号!\n";
// release 语义:确保此操作之前的所有写入对 acquire 一方可见
ready.store(true, std::memory_order_release);
}
int main() {
std::cout << "=== 1. 原子计数器示例 ===\n";
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(increment_worker);
}
for (auto& t : threads) {
t.join();
}
std::cout << "4 个线程各递增 10 万次,最终计数: "
<< counter.load() << "\n\n";
std::cout << "=== 2. acquire/release 同步示例 ===\n";
std::thread t1(waiter_thread);
std::thread t2(notifier_thread);
t1.join();
t2.join();
return 0;
}到此这篇关于深入理解C++ atomic库的文章就介绍到这了,更多相关C++ atomic库内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
