std::ofstream 写入日志时一行“断开”是因为 write() 是底层字节写入,不自动添加换行符,且若缓冲区满或未显式 flush,数据可能分多次写入,导致逻辑上的一行被拆分到不同物理行。

std::ofstream 写入日志时为什么一行会“断开”?
因为 std::ofstream::write() 和 操作符本身不是原子的——哪怕只写一行字符串,底层也可能被系统调度打断,尤其在多线程频繁调用时。更关键的是,C++ 标准库流对象(如 <code>std::ofstream)**不保证线程安全**,多个线程同时调用同一个 std::ofstream 对象的 operator,会导致缓冲区错乱、字符穿插、甚至 <code>std::ios_base::failure 异常。
用 std::mutex 保护 ofstream 是最直接的方案
别绕弯子,对共享日志文件句柄加锁是最可控、最易验证的做法。重点不是“要不要锁”,而是“锁的粒度和位置”:
- 锁必须包裹整个写入动作:从格式化字符串到
flush()(如果需要立即落盘),不能只锁operator 前半段 - 避免在锁内做耗时操作,比如拼接长字符串或调用
std::chrono::system_clock::now()—— 这些可以提前算好再进锁 - 不要用
std::scoped_lock自动管理就以为万事大吉;若日志函数可能递归调用(比如异常日志里又触发日志),得考虑死锁风险
示例片段:
std::mutex log_mutex;
std::ofstream log_file("app.log", std::ios::app);
<p>void write_log(const std::string& msg) {
auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::string line = "[" + std::ctime(&now) + "] " + msg + "\n"; // 提前拼好
std::lock_guard<:mutex> lk(log_mutex);
log_file </:mutex></p>
fprintf + FILE* 为什么有时比 ofstream 更稳?
因为 C 的 fprintf() 在大多数 libc 实现中(glibc、musl)对单个调用是线程安全的——它内部用了 per-FILE* 锁。但这不等于“无脑安全”:
-
fprintf()仅保证单次调用原子性,如果你分多次调用(比如先fprintf(fp, "[%s] ", time_str),再fprintf(fp, "%s\n", msg)),依然会穿插 - 必须用
setvbuf(fp, nullptr, _IONBF, 0)关闭缓冲,否则fflush()不调用就看不到实时内容;而开启全缓冲后,多线程 flush 仍需外部同步 - Windows 上
FILE*的线程安全性弱于 Linux,尤其在 Unicode 路径或宽字符场景下容易出问题
log4cpp / spdlog 等库没解决根本问题?
它们只是把锁封装得更隐蔽。比如 spdlog::daily_logger_mt 的 “mt” 后缀表示内部用了 std::mutex,但你仍要小心:
- 自定义 sink 如果没正确实现
sink::log()的线程安全,整条链路就破防 - 格式化开销(如
{:%Y-%m-%d %H:%M:%S})在锁内完成,高并发下会成为瓶颈 - 某些 sink(如网络 sink)失败时默认静默丢弃,你以为“写成功了”,其实日志根本没发出去
真正省心的做法是:用 spdlog::basic_logger_mt + 文件 sink,并确认其构造时传入的是独立 std::ofstream(而非共享),且禁用异步模式(除非你明确处理了队列满、线程崩溃等边界)。
最常被忽略的一点:日志行完整性不仅取决于写入线程安全,还依赖文件系统行为。ext4 默认启用 data=ordered,能保证单次 write() 系统调用的数据不被截断;但若程序崩溃在 write() 中途,或使用了带缓存的 SSD,仍可能出现半行。真要强保障,得上 O_SYNC 或 fsync(),代价是性能掉一个数量级。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











