std::atomic 默认线程安全且使用 memory_order_seq_cst,适用于绝大多数场景;仅在无锁编程或纳秒级优化时需手动指定内存序,如 relaxed 用于纯计数、acquire/release 用于同步原语。

直接用 std::atomic<int></int> 就够了,绝大多数场景根本不需要手动指定内存序 —— 除非你正在写无锁数据结构或对性能抠到纳秒级。
为什么 std::atomic<int></int> 默认构造就线程安全
它底层会根据平台自动选择合适的原子指令(x86 上是 lock xadd,ARM 上是 ldrex/strex),并隐式使用 memory_order_seq_cst。这个默认序保证:所有线程看到的原子操作顺序一致,且与非原子访问之间有全序约束。
常见误操作:
- 手动写
std::atomic<int> cnt{0}; cnt.fetch_add(1, std::memory_order_relaxed);</int>却没意识到:这会破坏“计数器值应反映真实调用次数”的语义 —— 其他线程可能因重排看不到最新值,甚至编译器都可能把后续依赖该值的判断提前执行 - 在日志计数、请求统计这类“只要总数不错就行”的场景,却死守
seq_cst,其实浪费了几十个周期
fetch_add 和 operator++ 的行为差异
两者都原子地加一,但返回值不同:fetch_add 返回旧值,operator++(后置)也返回旧值,前置 ++cnt 返回新值。实际选哪个,只取决于你是否需要那个值。
关键点:
-
cnt++等价于cnt.fetch_add(1),不是cnt.load() + 1; cnt.store(...)—— 后者完全不原子 - 如果用
cnt.load()判断是否达到阈值(比如限流),必须注意:两次load()之间值可能已变,这不是原子性问题,而是逻辑竞态,得靠更高层协议(如 CAS 循环)解决 - 不要为了“看起来简洁”而用
++cnt替代cnt.fetch_add(1),它们生成的汇编几乎一样,可读性反而后者更直白
什么时候真得调 memory_order
只有两类情况值得动它:
-
纯计数+最终汇总:比如埋点上报,只关心总和,不关心中间值是否实时可见 → 用
memory_order_relaxed。注意:必须确保没有其他线程依赖该计数器做分支决策(比如if (cnt > 100) flush();就不能用 relaxed) -
生产者-消费者信号量:比如用一个原子变量当标志位,生产者写完数据后
flag.store(true, memory_order_release),消费者用flag.load(memory_order_acquire)判断 —— 这时你操作的已不是“计数器”,而是同步原语,fetch_add反而不合适
错误示范:cnt.fetch_add(1, memory_order_acquire) —— acquire 是读端语义,对写操作加 acquire 没意义,编译器可能忽略或报错。
一个真正可用的线程安全计数器类
不炫技,不抽象,只封装最常踩坑的点:
class ThreadSafeCounter {
std::atomic<long> value_{0};
public:
// 默认用 seq_cst:安全、直观、性能损失在绝大多数场景下可忽略
void increment() { value_.fetch_add(1, std::memory_order_seq_cst); }
// 显式提供 relaxed 版本,加注释说明适用条件
void increment_relaxed() { value_.fetch_add(1, std::memory_order_relaxed); }
long get() const { return value_.load(std::memory_order_acquire); }
// reset 需要 store,用 release 保证之前的操作不会被重排到它之后
void reset() { value_.store(0, std::memory_order_release); }
};
</long>
重点不是代码多短,而是每个内存序选择都有明确理由,且暴露了 relaxed 的入口——逼你思考“我真需要它吗”。
别忘了:std::atomic 对齐要求比 int 高(通常需 4 或 8 字节对齐),如果把它塞进 packed struct 或手动内存池里,可能触发未定义行为。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











