双缓冲+专用flush线程是唯一兼顾性能、可靠性与可调试性的轻量方案;因std::ofstream每次flush或缓冲满时触发同步系统调用,且locale处理、格式化等均在业务线程执行,导致主线程卡顿。

直接用 std::ofstream 或 fwrite 同步写日志,主线程必然卡顿;双缓冲 + 专用 flush 线程是唯一能兼顾性能、可靠性与可调试性的轻量方案。
为什么 std::ofstream 写文件会卡主线程
它底层封装了带缓冲的 write(),但每次 flush() 或缓冲区满时仍触发同步系统调用;更糟的是,std::ofstream 的 locale 处理、格式化构造、异常路径都会在业务线程中执行,哪怕日志级别被关闭,开销也已产生。实测中,单次 log.info("req_id={}", id) 在高负载下可能拖慢主线程 0.2–3ms。
- 禁用
std::ofstream:不碰流式接口,跳过所有 iostream 隐式逻辑 - 改用裸
int fd+writev():一次提交多条日志,避免多次系统调用 - 预分配固定大小 buffer(如 64KB),用
std::array<char></char>而非std::vector——避免 realloc 破坏原子性 - 时间戳用
clock_gettime(CLOCK_MONOTONIC, &ts),不用std::chrono::system_clock::now()
如何用 std::atomic 实现无锁双缓冲切换
核心不是“两个数组”,而是两个指针所有权的原子移交。不能靠 std::mutex 锁整个 buffer,也不能在切换时 memcpy——这等于把 I/O 延迟挪回 CPU。
- 定义两块栈上 buffer:
std::array<char> buf_a, buf_b;</char> - 用
std::atomic<char> current_buf{buf_a.data()};</char>和std::atomic<size_t> used{0};</size_t>控制写入位置 - 写入线程只做:
char* b = current_buf.load(); memcpy(b + used.fetch_add(len), msg, len);,全程无锁无系统调用 - flush 线程调
current_buf.exchange(buf_b.data());拿走待刷 buffer,然后只读访问原 buffer 内容 - 切换前必须检查
used.load() + len >= buf_size,满则阻塞等待(或丢弃),绝不自旋空转
flush 线程该用 write() 还是 io_uring
除非你已稳定运行 kernel 6.0+ 且全链路适配 io_uring(包括 O_DIRECT、地址对齐、posix_memalign 分配),否则别碰 io_uring。它在日志场景下收益有限,但复杂度陡增,出错时 -EINVAL 或乱序 completion 极难定位。
- 首选「专用 flush 线程 +
writev()+fdatasync()」:简单、可 strace 调试、吞吐不输 AIO -
writev()把多条日志拼成一个struct iovec[]一次性提交,比逐条write()快 3–5 倍 -
fdatasync()比fsync()轻量,只刷数据不刷元数据,对日志文件足够安全 - 每累计 4KB 或每 100ms 主动
fdatasync(),别依赖O_SYNC(它会让每次write()都落盘) - 滚动日志时,用
std::shared_ptr<int></int>管理 fd,原子替换,避免旧 fd 继续写已 rename 的文件
宏定义怎么避免参数提前求值和格式化卡主线程
写成 LOG_INFO("user {} login", get_user_name()) 是典型错误:即使日志关了,get_user_name() 仍会执行,且格式化发生在业务线程。
- 用编译期过滤宏:
#define LOG_INFO(fmt, ...) do { if (LOG_LEVEL >= INFO) log_impl(INFO, __FILE__, __LINE__, fmt, ##__VA_ARGS__); } while(0) -
log_impl内部才做格式化,目标 buffer 必须是预分配栈空间(如char tmp[1024]),禁用std::string和std::ostringstream - 用
fmt::format_to替代snprintf,但 buffer 必须是fmt::memory_buffer或等效栈数组 - 文件名用
__FILE_NAME__(GCC 12+),省掉std::string::rfind('/') + substr()开销 - 短生命周期程序(如 CLI 工具)禁用异步,直接走同步模式,避免
main()退出后日志丢失
双缓冲真正难的不是交换逻辑,而是 buffer 切换瞬间的状态一致性——比如写入线程刚填满 buffer,flush 线程还没来得及取走,此时新日志到来,必须明确决定是丢弃、阻塞,还是 fallback 到临时堆分配并限流。这个策略没写进代码注释里,但决定了系统在峰值下的行为是否可控。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











