环形缓冲用std::vector加head/tail索引实现:head指向最老日志,tail指向下一写入位;写满时覆盖最老日志,读取需分段拼接。

用 std::vector + 两个索引实现最简环形缓冲
环形日志本质是固定大小的写入队列,不需要动态扩容、不追求线程安全时,std::vector 配合 head 和 tail 索引就足够。别一上来就用 std::deque 或锁住的 std::queue——前者内存不连续,后者无法控制容量上限。
关键点在于:写满后自动覆盖最老日志,读取时按逻辑顺序拼接两段内存(从 head 到末尾,再从开头到 tail-1)。
-
head指向最老有效日志位置(读起点),tail指向下一个空闲写入位置(写起点) - 写入前判断是否已满:
(tail + 1) % capacity == head,满则先head = (head + 1) % capacity - 读取全部日志时,若
head ,直接遍历 <code>[head, tail);否则分两段:[head, capacity)和[0, tail)
避免 std::string 在环形缓冲里反复拷贝
日志条目如果存 std::string 值语义对象,每次写入都会触发堆分配和复制,吞吐量立刻掉一半。尤其在高频打点场景下,这比缓冲逻辑本身更伤性能。
更合理的做法是只存日志的“视图”或“引用”,比如:
立即学习“C++免费学习笔记(深入)”;
- 用
std::string_view(C++17+),前提是原始字符串生命周期长于日志缓冲——适合格式化后暂存于栈或静态缓冲区的日志 - 用
char*+size_t组合,配合外部统一的临时日志缓冲区(如线程局部的 4KB 栈缓冲),写入时只拷贝指针和长度 - 如果必须存完整字符串,至少预分配
std::vector<:string></:string>并用reserve(),避免内部重复realloc
多线程写入时,std::atomic 足够,别急着上互斥锁
纯追加写入(无读操作干扰)场景下,只要保证 tail 更新和数据写入的原子性与顺序性,单个 std::atomic_size_t 就能替代 std::mutex。锁会把并发写变成串行,完全失去环形缓冲的意义。
典型错误是只原子更新 tail,却没约束数据写入顺序:
- 必须用
tail.fetch_add(1, std::memory_order_acquire)获取旧值,再用该值索引写入,最后才更新tail - 写入日志内容后,需
std::atomic_thread_fence(std::memory_order_release)确保内容对其他线程可见 - 读线程用
head.load(std::memory_order_acquire)获取当前头位置,再按前述逻辑读两段
注意:std::atomic 只保原子性,不保业务一致性——比如写入半条日志就被中断,得靠日志结构体自身带长度字段 + 校验位来识别脏数据。
日志落盘时机决定是否真“滚动”
环形缓冲只是内存层机制,真正滚动依赖落盘策略。很多人以为缓冲写满就自动刷盘,其实不是:如果每次写都 write() + fsync(),性能崩盘;如果完全不刷,进程崩溃就丢日志。
折中方案常见两种:
- 定时刷:用独立线程每 100ms 检查
tail != head,批量写入并write()(不fsync),靠 OS 缓冲扛住短时崩溃 - 触发刷:当缓冲区使用率超 80% 或日志含
"ERROR"字样时,立即write()+fsync(),确保关键日志不丢
文件名滚动(如 app.log.1, app.log.2)和环形缓冲无关,那是日志轮转(log rotation)的事,别混在一起实现。
最容易被忽略的是:环形缓冲的“滚动”只发生在内存索引层面,一旦写入位置绕回,旧数据就不可逆覆盖——没有后悔药,也没办法事后恢复。设计时就得想清楚,哪些日志值得留,哪些可以丢。










