双缓冲核心是实现无锁、无拷贝、无竞争的生产消费解耦:业务线程可立即写,日志线程安全读;std::queue+mutex因共用锁且磁盘阻塞会拖垮吞吐,高并发下易抖动甚至卡死主线程。

双缓冲不是开两块内存就完事,核心是让业务线程永远能“立刻写”,日志线程永远能“安全读”——中间不能有锁、不能有拷贝、不能有状态竞争。
为什么 std::queue + mutex 会拖垮吞吐
多线程高频打日志时,std::queue::push() 和 std::queue::pop() 共用同一把 std::mutex,实测在 16 核机器上超过 50k msg/s 就开始抖动。更糟的是:一旦磁盘慢(比如 fsync 卡住),消费者阻塞会导致生产者也卡住,业务线程直接被拖死。
- 别用
std::deque或std::vector做底层存储——push_back()可能触发 reallocate,破坏无锁前提 - 禁用
boost::lockfree::queue多生产者模式(MPMC)——它内部仍需原子操作争抢 slot,高并发下 cache line 伪共享严重 - 若必须用 boost,只选
boost::lockfree::spsc_queue,但得前置聚合层:多个业务线程先写入thread_local std::vector,再批量push()到 SPSC 队列
怎么组织双缓冲区才不丢日志又不卡主线程
缓冲区不是越大越好,也不是越小越快。关键在「写入边界对齐」和「切换语义完整」。
- 用固定大小的
std::array<char></char>(64KB)或自定义栈式 buffer,大小取 4KB~128KB,兼顾页对齐与 L1 缓存命中 - 禁止用
std::vector<char></char>—— realloc 会破坏原子性;也别裸 new[] —— 异常时内存泄漏难追踪 - 写入前必须检查剩余空间:
if (used.load() + len > buffer_size) { /* 触发切换或 fallback */ } - 切换时不能只交换指针,要确保最后一条日志结构体完整落进 buffer(例如
log_entry是 32 字节对齐的 struct,就得按 32 字节粒度推进写指针)
如何用 std::atomic 实现安全无锁切换
仅靠 std::atomic<char></char> 交换指针是危险的——没解决“正在写一半就被切走”的问题。必须引入状态机语义。
立即学习“C++免费学习笔记(深入)”;
- 用
std::atomic<int></int>表达三态:0=READY_TO_WRITE、1=SWITCHING、2=READY_TO_FLUSH - 业务线程写入前调
compare_exchange_weak(state, 0, 0)确认可写;若为1,自旋等待或 fallback 到thread_local char[4096]暂存 - 日志线程检测到满/超时后,先
compare_exchange_strong(state, 0, 1),再原子交换 buffer 指针,最后置state = 2 - 绝不用
std::atomic_flag模拟锁——它无法表达 “刷盘中” 这个中间态,容易漏掉未完成刷盘就重用 buffer 的崩溃场景
write() 调用前最容易被忽略的三个坑
就算缓冲区无锁、切换正确,write() 本身也可能因配置或调用方式翻车。
- 打开文件时禁用
O_SYNC和O_DSYNC—— 它们会让每次write()等磁盘确认,彻底废掉双缓冲意义 - 务必加
O_APPEND | O_WRONLY | O_CREAT,但注意:在 NFS 或某些分布式文件系统上O_APPEND可能退化为用户态锁,此时应改用 pre-allocated file + offset tracking - 单次
write()不要小于 4KB —— 小于页大小易触发内核零拷贝优化失败;大于 1MB 则可能被调度器中断;推荐攒够 64KB 再提交,或用writev()批量写多个 log_entry 的 iovec
真正难的不是实现双缓冲,而是守住「写入不阻塞」「切换不撕裂」「刷盘不丢数据」这三条底线。很多库在压力测试里扛不住,不是算法错,是忘了 std::atomic_thread_fence(std::memory_order_release) 该插在哪,或是 posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) 没在 flush 后及时调用。



















