c++中如何使用std::atomic实现无锁计数器_c++原子操作实例【实例】

std::atomic做计数器足够安全且够用,但必须正确使用原子操作、避免取地址或memcpy、显式调用load/store、按需选择memory_order(如relaxed),且T必须trivially_copyable。

std::atomic 做计数器足够安全吗

够用,但得用对操作。直接用 +++= 是安全的,因为 std::atomic 重载了这些运算符,底层调用的是原子加法指令(如 x86 的 lock xadd)。但别把它当普通 int 用——比如取地址后传给非原子函数、或用 memcpy 拷贝,会破坏原子性保证。

常见错误现象:

std::atomic cnt{0};
int* p = &cnt;  // ❌ 危险!获取内部值地址,失去原子语义
*p = 1;         // 非原子写入,竞态风险

  • 只通过 load()store()fetch_add()operator++ 等成员函数访问
  • 避免隐式转换:不写 int x = cnt;,而用 int x = cnt.load(); 显式表达意图
  • 默认内存序是 std::memory_order_seq_cst,安全但稍慢;高并发场景可考虑 relaxed(仅计数,不依赖顺序)

多线程递增时为什么还要指定 memory_order

因为不同内存序影响性能和可见性边界。计数器本身只要求“加法不丢失”,不关心和其他变量的执行顺序,这时用 std::memory_order_relaxed 就够了——CPU 不会插入多余屏障,吞吐更高。

对比示例:

std::atomic cnt{0};

// 默认:强顺序,安全但有开销 cnt++;

// 推荐(纯计数场景): cnt.fetch_add(1, std::memory_order_relaxed);

// 错误用法(混合顺序): cnt.fetch_add(1, std::memory_order_relaxed); cnt.load(std::memory_order_acquire); // acquire 和 relaxed 搭配无意义,易误导

  • relaxed:只保证该原子操作自身不被重排,不建立同步关系
  • acquire/release:用于保护临界资源(如指针解引用前需确保对象已构造完成),计数器一般不需要
  • 除非你要用计数器做“信号量”(比如等 cnt 达到 N 才继续),否则别默认用 seq_cst

std::atomic 能当开关用吗

能,但别用 operator=operator== 直接赋值比较——虽然语法合法,但容易写出非预期行为。例如:

std::atomic ready{false};

// ❌ 危险写法(可能被编译器优化掉读取): while (!ready) { / busy wait / }

// ✅ 正确写法: while (!ready.load(std::memory_order_acquire)) { / ... / }

// ✅ 更推荐(带提示,减少空转功耗): while (!ready.load(std::memory_order_acquire)) { std::this_thread::yield(); }

  • std::atomic 不支持 ++,也不支持算术操作,只适合标志位
  • 读写都应显式指定内存序;store() 通常用 releaseload()acquire,构成同步对
  • 注意:std::atomic_flag 更轻量(无锁保证更强),但只能做 test-and-set,不如 bool 直观

std::atomic 的 T 必须满足

trivially copyable 吗

必须。否则编译失败。这意味着你不能把 std::stringstd::vector 或带虚函数/自定义构造函数的类塞进 std::atomic

错误示例:

struct NonTrivial {
    std::string s; // ❌ string 构造/析构非平凡
};
std::atomic x; // 编译错误

  • 内置类型(intlong long)、enum、POD 结构体(无构造函数、无虚函数、所有成员 trivial)可以
  • 不确定是否 trivial?查 std::is_trivially_copyable_v
  • 想原子更新复杂对象?用 std::atomic<:shared_ptr>> 或配合 mutex,别硬套 atomic

实际写无锁计数器,最常踩的坑不是不会调用 fetch_add,而是忘了内存序语义、误以为 atomic 能“自动同步其他变量”、或者试图原子化非 trivial 类型。这些地方一错,问题往往在高并发下才暴露,且极难复现。