不能直接在多线程里用 std::ofstream 写日志,因为其流对象本身非线程安全,多线程并发调用 operator

为什么不能直接在多线程里用 std::ofstream 写日志
多个线程同时调用 operator 写同一个 <code>std::ofstream 对象,会触发未定义行为——不是简单地乱序或丢日志,而是可能崩溃、内存越界或静默损坏文件。C++ 标准库的流对象本身不保证线程安全,哪怕你加了锁,std::ofstream 内部缓冲区和系统 write 调用之间仍有竞态窗口。
常见错误现象:std::bad_alloc 突然抛出、日志文件出现二进制乱码、某条日志被截断成半行、程序在 flush() 时卡死。
- 别用全局
std::ofstream+ 手动std::mutex锁住整个写操作——性能差,且无法避免 flush 时机不可控 - 别在每个线程里各自打开/关闭同名文件——文件系统不保证原子追加,尤其在 ext4/xfs 等文件系统上容易覆盖或丢数据
- 别依赖
std::ios_base::app模式就认为“安全”——它只保证每次write系统调用以追加方式进入内核,但多线程并发调用仍可能交错
推荐方案:无锁环形缓冲区 + 单独日志线程
核心思路是把“记录日志”和“写入磁盘”解耦:业务线程只往内存缓冲区塞格式化好的日志字符串(无锁),由一个专用线程负责批量刷盘。这是高性能异步日志的通用范式,g3log、spdlog 的 async mode 都基于此。
关键实现点:
- 用
boost::lockfree::spsc_queue或自研的单生产者单消费者无锁队列(SPSC),比std::queue + std::mutex延迟低一个数量级 - 日志消息结构体必须是 trivially copyable,避免构造/析构开销;建议预分配固定大小缓冲(如 512 字节),避免堆分配
- 业务线程中不要做时间戳格式化、文件名拼接等耗时操作——这些应由日志线程统一处理,否则异步就失去意义
- 日志线程需监听队列并主动
flush(),不要依赖std::endl(它强制 flush);可按条数(如每 64 条)或时间间隔(如 10ms)批量写入
示例片段(简化版 SPSC 队列写入):
struct LogMsg {
char data[512];
uint32_t len;
uint64_t timestamp; // 纳秒级,由业务线程 clock_gettime(CLOCK_MONOTONIC, ...) 获取
};
// 生产者(任意线程)
LogMsg msg{};
snprintf(msg.data, sizeof(msg.data), "[%ld] %s", msg.timestamp, "user login");
msg.len = strlen(msg.data);
log_queue.push(msg); // lock-free push
spdlog 异步模式怎么配才真正生效
很多人调了 spdlog::basic_logger_mt<:async_factory></:async_factory> 就以为万事大吉,但实际仍可能同步阻塞——根本原因是没正确设置队列大小和溢出策略。
- 默认队列容量只有 8192 条,高负载下极易满;满后行为取决于
overflow_policy:默认是block(阻塞线程),这会让业务线程卡住,完全违背异步初衷 - 务必显式设置
spdlog::set_async_mode(8192*4, spdlog::async_overflow_policy::overrun_oldest),让旧日志被覆盖而非阻塞 - 如果用
spdlog::rotating_logger_mt,确保max_files和max_file_size合理,否则日志线程在轮转文件时可能短暂阻塞(尤其是 rename + open 新文件) - 禁用
spdlog::cfg::load_from_file()中的async字段自动推导——它可能忽略你的溢出策略,老老实实代码里 set
错误配置示例(看似异步,实则可能阻塞):
// ❌ 危险!没设 overflow_policy,满队列时 block
auto logger = spdlog::basic_logger_mt<:async_factory>("async_logger", "logs.txt");
// ✅ 正确:显式控制溢出行为
spdlog::set_async_mode(32768, spdlog::async_overflow_policy::overrun_oldest);
auto logger = spdlog::basic_logger_mt("async_logger", "logs.txt");
</:async_factory>
什么时候该放弃异步日志
异步日志不是银弹。以下场景强行上异步反而增加复杂度甚至风险:
- 调试阶段需要 100% 日志保真和精确时序:异步模型天然有延迟(毫秒级),且日志线程 crash 会导致缓冲区日志永久丢失
- 嵌入式或内存受限环境(如 RAM writev() 批量写更稳
- 日志量极低(std::shared_mutex 保护
std::ofstream更简单,避免引入额外线程调度开销 - 需要与 crash dump 强绑定(如 segfault 发生瞬间必须落盘):异步日志线程可能还没来得及处理缓冲区,应改用
signal handler+write()(仅限 async-signal-safe 函数)直接写文件
真正难的是权衡——比如金融交易系统要求日志不丢,就得在异步基础上加双缓冲+持久化确认机制;而游戏客户端只要不卡帧,用 spdlog 默认异步配置足矣。选型前先测压:用 std::chrono::high_resolution_clock 在热点路径打点,看日志调用是否成为瓶颈。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











