直接用 int 做多线程计数器会导致竞态,因 i++ 非原子的“读-改-写”三步易被中断,引发结果丢失;应使用 std::atomic 并避免伪共享与不当内存序。

为什么不能直接用 int 做多线程计数器
多个线程同时执行 i++ 时,实际是“读-改-写”三步,中间可能被其他线程打断。即使编译器没优化,CPU 的缓存行(cache line)和指令重排也会导致结果丢失。常见现象是:10 个线程各加 10000,最终结果远小于 100000,且每次运行结果还不一样。
std::atomic<int></int> 是最简无锁计数器的基础
它保证单个操作(如 fetch_add)是原子的,底层通常编译为 lock xadd(x86)或 ldxr/stxr(ARM),无需互斥锁就能同步。
实操建议:
- 声明必须用
std::atomic<int> counter{0};</int>,不能用int临时转 —— 那会失去原子性 - 避免使用
operator++()(前缀)或operator++(int)(后缀),它们隐含读取旧值,语义不如显式调用清晰 - 真正需要“加完返回新值”时,用
counter.fetch_add(1, std::memory_order_relaxed) + 1;仅需累加就用counter.fetch_add(1, std::memory_order_relaxed) -
std::memory_order_relaxed足够用于纯计数场景 —— 它不约束前后内存访问顺序,性能最高;只有在计数结果要作为同步信号(比如控制某段逻辑是否执行)时,才考虑acquire/release
避免伪共享(false sharing)拖慢性能
当多个 std::atomic<int></int> 变量落在同一 cache line(通常是 64 字节)里,一个核修改其中一个,会导致其他核的对应 cache line 失效,频繁同步反而比加锁还慢。
解决方法:
- 手动填充:用
alignas(64)对齐,并在变量前后加char pad[64](注意别影响结构体大小计算) - 更稳妥的做法是每个计数器独占一个 cache line,例如定义为
struct alignas(64) PaddedCounter { std::atomic<long> val{0}; };</long> - 不要把多个计数器塞进同一个结构体还只对结构体对齐 —— 成员仍可能挤在同一行
什么时候该放弃原子计数器
如果计数逻辑复杂,比如“加 1 后检查是否达到阈值,达到则触发回调”,原子操作本身无法涵盖整个条件逻辑,强行用 compare_exchange_weak 循环容易写错,且竞争激烈时自旋开销大。
此时更合理的选择是:
- 用
std::mutex+ 普通int,实测在低争用( - 若必须无锁且逻辑复杂,考虑
std::atomic<:shared_ptr>></:shared_ptr>或 hazard pointer 等更高级模式,但复杂度陡增 - 别为了“无锁”而无锁 —— 正确性、可维护性、真实负载下的吞吐才是关键
最常被忽略的一点:原子变量不是银弹,std::atomic_flag 才是最轻量的无锁原语;std::atomic<int></int> 在某些平台(如旧 ARM)上可能退化为基于锁的实现,务必在目标环境验证生成的汇编。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











