一行日志被截断是因为posix write系统调用不保证行级原子性,多线程并发写入时内容会交错;需用mutex保护整行格式化与单次写入。

log4cpp / spdlog 多线程写文件时为什么一行日志会被截断?
因为底层 fwrite 或 std::ofstream::write 本身不保证行级原子性——哪怕你传入一整行字符串,内核可能在 write 系统调用中途被调度切走,另一个线程紧接着写入,结果两行内容交错混在一起。这不是日志库的 bug,是 POSIX I/O 的基本事实。
常见错误现象:2024-05-12 10:23:41 INFO User login success 和 2024-05-12 10:23:41 ERROR DB timeout 合并成 2024-05-12 10:23:41 INFO User login success2024-05-12 10:23:41 ERROR DB timeout,甚至更糟:字符级穿插。
- 别指望
std::endl或\n自动加锁——它只负责刷缓冲区,不防并发写 - 不要手动对每个
operator 调用加锁(比如 <code>log 分三次写),中间可能被其他线程插入 - spdlog 默认的
basic_file_sink_mt是线程安全的,但“安全”指“不会崩”,不等于“每行完整”——它仍依赖底层 write 原子性,而普通文件不提供行原子保障
用 std::mutex + std::ostringstream 构造完整行再写入
核心思路:把整行日志拼成一个 std::string(或 std::ostringstream),然后单次 write() 或 到文件流。锁只保护拼接和写入这两个动作,不锁整个日志生命周期。
实操建议:
- 用
std::mutex(非std::recursive_mutex)保护共享的std::ofstream或 sink 对象 - 拼接必须在锁内完成:先格式化时间、级别、消息,再合成完整字符串,最后一次性写入
- 避免在锁内做耗时操作(如网络调用、磁盘 stat)
- 示例关键片段:
std::lock_guard<:mutex> lock(mtx_); oss_ </:mutex>
为什么 mmap + O_APPEND 不解决这个问题?
O_APPEND 只保证每次 write() 的偏移定位是原子的(即不会覆盖别人),但不保证内容不被其他线程的 write() 插入中间;mmap 更危险——多个线程直接往同一块内存映射区写,连换行符都可能被踩碎。
性能与兼容性影响:
-
O_APPEND在 NFS 或某些容器文件系统上可能退化为普通 seek+write,失去原子性语义 -
mmap写日志需手动处理同步(msync)、大小扩展、多进程可见性,复杂度远超收益 - 真正需要高性能且强完整性时,应考虑无锁环形缓冲区 + 单独日志线程消费(如 g3log),而非在写入点硬扛并发
第三方库中哪些 sink 真正做到了行完整?
不是所有标称 “mt”(multi-threaded)的 sink 都默认保行完整。关键看它是否把格式化+写入封装为不可分割单元。
- spdlog 的
rotating_logger_mt+basic_file_sink_mt:默认行为是线程安全拼接+单次 write,满足行完整(前提是不自定义 sink 且不用异步模式) - log4cpp 的
FileAppender:默认非线程安全,必须显式包装SynchronizedAppender,否则即使加锁也难保行完整(内部可能分多次 write) - boost.log:需启用
locking并配置text_file_backend的rotation_size和auto_flush,否则缓冲区可能跨线程污染 - 注意:异步 logger(如 spdlog::async_logger)会把格式化和写入拆到不同线程,此时行完整性由队列项粒度决定——只要每条日志消息是一个完整字符串入队,就仍是安全的
最容易被忽略的是:日志内容本身含换行符(比如打印 stack trace 或 JSON 字符串)。这种情况下,“一行”已失去意义,必须靠上层协议(如用 \0 分隔、加长度头)或改用结构化日志格式(JSON 行)来界定边界。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











