用 std::atomic 实现每秒计数需避免锁、sleep 和 system_clock,采用 steady_clock 对齐整秒边界,用 exchange 原子获取并重置,注意缓存行伪共享,每个计数器单独 64 字节对齐。

用 std::atomic 做每秒计数,别碰锁
直接用 std::atomic<long long></long> 累加调用次数,比加互斥锁快一个数量级,且无上下文切换开销。多线程下 fetch_add 是原子的,不需要额外同步。
常见错误是用普通 int 或 long 变量++,在高并发下会丢计数——这不是“偶尔不准”,而是必现问题。
- 初始化用
std::atomic<long long> count{0}</long>,别用int(溢出太快) - 每次调用只做
count.fetch_add(1, std::memory_order_relaxed),relaxed足够,不需acq_rel - 避免在 hot path 里调用
std::time()或std::chrono::steady_clock::now(),它们本身有开销
每秒清零不能靠 sleep(1)
用 std::this_thread::sleep_for() 定时清零,会导致统计窗口漂移:比如上一秒从 1.2s 开始,下一秒就从 2.2s 开始,漏掉或重复统计部分请求。
正确做法是用单调时钟对齐整秒边界,靠时间戳差值判断是否跨秒。
立即学习“C++免费学习笔记(深入)”;
- 记录上次重置时间
last_reset_time(std::chrono::steady_clock::time_point) - 每次读取计数前,先算当前秒数:
auto now = steady_clock::now(); auto curr_sec = duration_cast<seconds>(now.time_since_epoch()).count();</seconds> - 如果
curr_sec != last_sec,才交换并重置计数器,同时更新last_sec - 不要用
system_clock,它可能被系统管理员调整,导致秒数跳变
std::atomic::exchange() 是获取+重置的唯一安全方式
想拿到上一秒的计数值并清零,不能分两步:先 load() 再 store(0)——中间可能被其他线程增加,造成漏计。
exchange() 是原子的“读旧值、写新值”操作,没有竞态窗口。
- 用
count.exchange(0, std::memory_order_relaxed)一次性拿到上一秒累计值 - 返回值就是你要上报的 QPS,原地归零,无需锁也不依赖顺序
- 别用
compare_exchange_weak循环模拟,纯属多此一举 - 注意:返回的是
long long,别直接赋给int导致截断
小心 std::atomic 的内存序和缓存行伪共享
单个 std::atomic<long long></long> 没问题,但如果你把多个计数器(比如按接口名分组)塞进一个 Struct 里挨着放,不同线程频繁更新相邻字段,会触发 CPU 缓存行(64 字节)反复无效化,性能暴跌。
这不是理论风险,在千核服务器上实测过 QPS 掉 40% 以上。
- 每个高频计数器之间至少 padding 64 字节,例如:
alignas(64) std::atomic<long long> qps_a;</long> - 别把计数器和非原子字段(如字符串名、配置标志)混在一个 struct 里共用缓存行
- 如果用数组存 N 个计数器,确保
sizeof(atomic) * N不跨缓存行边界——更稳妥是每个单独alignas(64) - Clang/GCC 下可用
__attribute__((aligned(64)))强制对齐
真正难的不是写出来,是压测时发现 QPS 曲线毛刺多、波动大,最后追到缓存行争用上——这个点没人提,但线上最容易栽。