一、std::atomic 到底解决了什么问题?
多线程编程里,最让人头疼的就是数据竞争。你想想看,两个线程同时读写同一个变量,结果会怎样?
我早年做游戏服务器时,就踩过这个坑。一个简单的计数器,两个线程各加 100 万次,结果跑出来不是 200 万。当时我盯着屏幕愣了半天——嗯,这就是典型的「非原子操作」问题。
std::atomic 的出现,就是为了解决这个。它保证了对变量的读写是「不可分割」的。说白了,要么读完了再写,要么写完了再读,不会出现读到一半的值。
二、原子类型一览
C++11 标准库提供了一组原子类型,都在 <atomic> 头文件里。常用的有这些:
| 原子类型 | 对应的普通类型 | 说明 |
|---|---|---|
| std::atomic<bool> | bool | 原子布尔值 |
| std::atomic<int> | int | 原子整型,最常用 |
| std::atomic<unsigned int> | unsigned int | 无符号版本 |
| std::atomic<long long> | long long | 64位原子整型 |
| std::atomic<T*> | T* | 原子指针 |
| std::atomic_flag | — | 最简单的原子标志,保证无锁 |
我个人习惯用 std::atomic<int> 和 std::atomic<bool> 解决 90% 的场景。剩下的 10%,才需要上 CAS 或者自定义原子类型。
三、CAS 操作:原子世界的「比较-交换」
CAS 全称是 Compare-And-Swap,也叫 Compare-And-Exchange。它的逻辑很简单:
bool atomic_compare_exchange_weak(T* expected, T desired);
bool atomic_compare_exchange_strong(T* expected, T desired);
什么意思呢?就是「如果当前值等于 expected,就把它换成 desired,返回 true;否则把当前值写回 expected,返回 false」。
我在项目中遇到过这样一个场景:多个线程要抢一个任务队列里的任务。用 CAS 实现一个无锁的 pop 操作,比用 mutex 快了好几倍。
std::atomic<int> counter{0};
void thread_func() {
int expected = 0;
int desired = 1;
// 尝试把 counter 从 0 改成 1
if (counter.compare_exchange_strong(expected, desired)) {
// 成功了,说明我是第一个
} else {
// 失败了,expected 被更新为当前值
}
}
四、内存屏障:看不见的秩序
原子操作只是保证了「原子性」,但没保证「顺序性」。你想想看,CPU 和编译器为了优化,可能会重排指令。这就引出了内存屏障的概念。
std::atomic 的每个操作都可以指定一个内存序(memory_order),常见的有:
- memory_order_relaxed:最宽松,只保证原子性,不保证顺序
- memory_order_acquire:之后的读写不能重排到该操作之前
- memory_order_release:之前的读写不能重排到该操作之后
- memory_order_acq_rel:acquire + release 的结合
- memory_order_seq_cst:最严格,全局顺序一致(默认值)
我曾经在优化一个高频交易系统时,把默认的 seq_cst 改成了 acquire-release,性能提升了 30%。但代价是——你得非常清楚自己在做什么。
五、无锁数据结构基础
无锁数据结构,说白了就是不用 mutex,全靠原子操作和 CAS 来保证线程安全。它的核心思想是:如果操作失败,就重试,而不是阻塞。
一个最简单的无锁栈:
template<typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head{nullptr};
public:
void push(T value) {
Node* new_node = new Node{value, nullptr};
new_node->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(
new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed)) {
// 重试,直到成功
}
}
bool pop(T& value) {
Node* old_head = head.load(std::memory_order_relaxed);
while (old_head && !head.compare_exchange_weak(
old_head, old_head->next,
std::memory_order_acquire,
std::memory_order_relaxed)) {
// 重试
}
if (old_head) {
value = old_head->data;
delete old_head;
return true;
}
return false;
}
};
这段代码看起来简单,但有个坑——ABA 问题。什么意思呢?就是线程 A 读到了 head 指向节点 X,然后线程 B 把 X 弹出去又 push 回来,head 还是指向 X,但 X 的内容已经变了。线程 A 的 CAS 会成功,但拿到的数据是错的。
六、知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一张地图,随时回来查阅。
七、总结与建议
std::atomic 是 C++ 并发编程的基石。我个人觉得,掌握它需要分三步走:
- 先会用:知道怎么声明原子变量,怎么读写
- 再理解:搞懂 CAS 和内存序的含义
- 最后优化:在性能敏感的场景下,选择合适的 memory_order
记住一点:无锁编程不是银弹。它比 mutex 更难调试,更容易出错。我见过太多人一上来就搞无锁队列,结果 bug 修了一个月。如果你只是需要保护一个共享变量,用 std::atomic 就够了。如果你需要保护一段代码,老老实实用 std::mutex。