fetch_add比加锁计数快,因其在单条cpu指令中完成读-改-写,避免上下文切换与内核态开销,缓存命中时仅需几十周期,而锁操作通常需数百周期以上。

fetch_add 为什么比加锁计数快
fetch_add 是 std::atomic 提供的原子读-改-写操作,它在单条 CPU 指令(如 x86 的 xadd 或 ARM 的 ldadd)中完成“读取当前值 → 加上指定增量 → 写回 → 返回旧值”全过程。这避免了互斥锁(std::mutex)带来的上下文切换、内核态陷入和排队等待开销。
关键点在于:只要目标变量是缓存在 CPU 核心的 L1 cache 中,fetch_add 通常只需几十个周期;而一次轻量锁的 lock/unlock 在争用不激烈时也要数百周期,争用高时可能飙升到微秒级。
注意:不是所有类型都支持 fetch_add —— 仅限整型(int, long, size_t 等)和指针类型;浮点型需用 fetch_add 的特化版本(C++20 起)或退回到 compare_exchange_weak 循环。
正确声明和初始化 atomic 计数器
必须显式指定内存序(memory order),否则默认为 std::memory_order_seq_cst,安全但略重。对纯计数场景,通常可用更宽松的序:
常见初始化方式:
std::atomic<int> counter{0}; // OK,值初始化为 0
std::atomic<long> total_bytes{0L}; // 注意字面量类型匹配
std::atomic<size_t> request_count{}; // 值初始化,等价于 = 0</size_t></long></int>
容易踩的坑:
- 忘记初始化:未初始化的
std::atomic<int></int> 对象值是未定义的,不是 0
- 用
= 0 初始化非 trivial 类型(如自定义 struct)会编译失败;只对基础整型/指针安全
- 跨线程访问前未确保对象生命周期——比如在主线程构造后,子线程才开始读写,但对象若在子线程启动前就析构了,行为未定义
fetch_add 的参数和返回值怎么用
fetch_add 接收一个增量参数(可正可负),返回操作前的旧值。典型用法是“先取旧值,再基于它做逻辑判断”。
例如实现带阈值的计数上报:
if (counter.fetch_add(1, std::memory_order_relaxed) == 99) {
report_to_server(counter.load(std::memory_order_relaxed));
}
这里用 std::memory_order_relaxed 是因为计数本身不要求与其他内存操作同步,只关心原子性。但要注意:
- 返回的是“加之前的值”,所以
== 99 表示本次加 1 后变成 100
- 多个线程可能同时看到
fetch_add 返回 99(竞态),导致多次上报;若需严格一次,得配合 compare_exchange 或额外锁
-
std::memory_order_relaxed 不禁止编译器/CPU 重排,若该计数和其他变量(如标志位)有逻辑依赖,必须升级内存序(如 acq_rel)
性能差异在哪?什么时候不该用 fetch_add
在低争用(线程数 ≤ CPU 核心数)且操作简单(仅增减)时,fetch_add 比锁快 3–10 倍;但在高争用下,缓存行频繁在核心间传递(false sharing),反而可能比锁慢。
需要警惕的场景:
- 多个原子变量紧挨着定义(如
std::atomic<int> a, b, c;</int>),可能落在同一缓存行(64 字节),造成 false sharing —— 解决方法是用 alignas(64) 隔离
- 需要复合操作:比如“如果小于阈值则加 1,否则跳过”,这时
fetch_add 不够用,得用 compare_exchange_weak 循环
- 计数结果要立刻用于控制流(如 if 分支),且分支结果影响后续内存访问,则需更强的内存序保证,不能无脑用
relaxed
真正难的不是写对 fetch_add,而是判断它是否适合你的数据流语义和并发模型——尤其当计数嵌套在更大状态机里时,内存序和可见性边界很容易被忽略。
std::memory_order_seq_cst,安全但略重。对纯计数场景,通常可用更宽松的序:
常见初始化方式:
std::atomic<int> counter{0}; // OK,值初始化为 0
std::atomic<long> total_bytes{0L}; // 注意字面量类型匹配
std::atomic<size_t> request_count{}; // 值初始化,等价于 = 0</size_t></long></int>
容易踩的坑:
- 忘记初始化:未初始化的
std::atomic<int></int>对象值是未定义的,不是 0 - 用
= 0初始化非 trivial 类型(如自定义 struct)会编译失败;只对基础整型/指针安全 - 跨线程访问前未确保对象生命周期——比如在主线程构造后,子线程才开始读写,但对象若在子线程启动前就析构了,行为未定义
fetch_add 的参数和返回值怎么用
fetch_add 接收一个增量参数(可正可负),返回操作前的旧值。典型用法是“先取旧值,再基于它做逻辑判断”。
例如实现带阈值的计数上报:
if (counter.fetch_add(1, std::memory_order_relaxed) == 99) {
report_to_server(counter.load(std::memory_order_relaxed));
}
这里用 std::memory_order_relaxed 是因为计数本身不要求与其他内存操作同步,只关心原子性。但要注意:
- 返回的是“加之前的值”,所以
== 99表示本次加 1 后变成 100 - 多个线程可能同时看到
fetch_add返回 99(竞态),导致多次上报;若需严格一次,得配合compare_exchange或额外锁 -
std::memory_order_relaxed不禁止编译器/CPU 重排,若该计数和其他变量(如标志位)有逻辑依赖,必须升级内存序(如acq_rel)
性能差异在哪?什么时候不该用 fetch_add
在低争用(线程数 ≤ CPU 核心数)且操作简单(仅增减)时,fetch_add 比锁快 3–10 倍;但在高争用下,缓存行频繁在核心间传递(false sharing),反而可能比锁慢。
需要警惕的场景:
- 多个原子变量紧挨着定义(如
std::atomic<int> a, b, c;</int>),可能落在同一缓存行(64 字节),造成 false sharing —— 解决方法是用alignas(64)隔离 - 需要复合操作:比如“如果小于阈值则加 1,否则跳过”,这时
fetch_add不够用,得用compare_exchange_weak循环 - 计数结果要立刻用于控制流(如 if 分支),且分支结果影响后续内存访问,则需更强的内存序保证,不能无脑用
relaxed
真正难的不是写对 fetch_add,而是判断它是否适合你的数据流语义和并发模型——尤其当计数嵌套在更大状态机里时,内存序和可见性边界很容易被忽略。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











