双缓冲性能关键在于零等待提交、批量writev、绕过page cache及严格内存序与结构对齐;需三状态原子机控制缓冲区切换、posix_memalign+mlock预映射、禁用o_sync/o_dsync、按场景选64kb或4mb缓冲区,并确保log_entry alignas(8)及内存栅栏。

双缓冲本身不提升吞吐量,真正起效的是让业务线程零等待、批量提交 writev、绕过 page cache 干扰,并严守内存序与结构对齐——否则 10 万条/秒都难突破,还容易丢日志或崩溃。
三状态原子机控制缓冲区切换,别只用 std::atomic 交换指针
仅靠 std::atomic<char></char> 交换 front/back 指针,业务线程 memcpy 到一半就被刷盘线程读取,必然出现字节撕裂、字段错位甚至段错误。必须引入明确生命周期状态:
-
READY_TO_WRITE:业务线程可安全追加,append()前检查state_.load() == 0 -
SWITCHING:中间态,禁止任何读写;业务线程自旋超 100 次后 fallback 到thread_local char tmp[4096] -
READY_TO_FLUSH:刷盘线程只读消费,且要求used_len_已对齐到struct log_entry边界 - 状态跃迁必须用
compare_exchange_weak,失败不重试,直接走临时缓冲
posix_memalign 对齐分配 + mlock 预映射,避免 write() 被缺页中断卡住
用 new char[65536] 或 std::vector<char></char> 分配 buffer,首次 write() 可能触发缺页异常,延迟从微秒级跳到毫秒级,尤其在 4MB 大 buffer 场景下不可控:
- 必须调用
posix_memalign(&buf_a_, 4096, size_),地址按 4KB 对齐 - 分配后立即
mlock(buf_a_, size_)(需CAP_IPC_LOCK),失败则memset(buf_a_, 0, size_)强制映射 -
write()传入的 buf 指针必须是posix_memalign返回的原始地址,偏移后内核返回-EINVAL - 两块 buffer 生命周期一致,禁止 realloc、禁止 vector 扩容
禁用 O_SYNC/O_DSYNC,用 writev + fdatasync 分层刷盘
只要文件以 O_SYNC 或 O_DSYNC 打开,双缓冲就彻底失效——每次 write() 都等磁盘确认,吞吐归零:
- 打开日志文件用
O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC,绝对不用O_DIRECT(小日志对齐成本高) - 刷盘线程收集完整日志项,构造成
std::vector<iovec></iovec>,一次性writev(fd_, iov.data(), iov.size()) -
fdatasync(fd_)只刷数据页,不刷元数据,延迟比fsync()低 30%~50% -
fdatasync()仅在三种时机触发:buffer 满、空闲超 1s、显式flush()
缓冲区大小与刷盘线程策略,不是越大越快
64KB 和 4MB 缓冲区适用场景完全不同,硬套参数反而拖垮性能:
- 64KB(65536 字节)适合 SSD + 高频小日志:
write()延迟稳定在 10–50μs,CPU 开销低,利于 L1/L2 缓存局部性 - 4MB 适合机械盘或超高密度日志(如每秒百万条):减少
write()次数,但单次刷盘可能卡住 5ms+,需配合posix_fadvise(fd_, 0, 0, POSIX_FADV_DONTNEED) - 刷盘线程必须是专用线程,循环中先
swap_out_full_buffer(),再writev(),最后fdatasync(),不能混用std::ofstream或fwrite() - 后台线程析构前必须设
stop_requested_.store(true)并join(),否则std::thread析构直接调用std::terminate
最容易被忽略的是:log_entry 结构体必须 alignas(8),且每条日志写入后必须用 std::atomic_thread_fence(std::memory_order_release) 确保对刷盘线程可见;否则即使状态机正确,也可能读到半截结构体。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











