因为 std::ofstream 不是线程安全的,多个线程同时调用其 write() 等成员函数会引发数据竞争,导致未定义行为、崩溃或日志错乱。

为什么直接用 std::ofstream 多线程写日志会崩
多个线程同时调用 std::ofstream::write() 或 std::ios_base::failure 异常或进程 abort。C++ 标准明确不保证 std::ofstream 对象的线程安全性 —— 即使是不同对象写同一文件,底层 fd 共享也会引发 write() 系统调用竞争。
- Windows 上常见
Invalid argument错误(errno = 22),源于多线程对 FILE* 的非原子 seek + write 组合 - Linux 下可能观察到部分日志行被截断,或出现
Bad file descriptor(errno = 9) - 跨平台时别依赖
flock()或LockFileEx()做临界区保护:前者在 NFS 上不可靠,后者 Windows-only
用无锁队列把日志“收进来”,但别真无锁到底
纯无锁队列(如基于 CAS 的 ring buffer)能避免入队阻塞,但出队端仍需同步 —— 日志落地必须串行化,否则顺序无法保障。推荐用 moodycamel::ConcurrentQueue(轻量、免锁入队、有内存序控制),搭配单消费者线程做落盘。
- 队列元素建议用
std::string而非char*:避免生命周期管理错误;构造时用std::string_view避免拷贝 - 设置合理容量(如 65536),满时采用丢弃策略(
try_enqueue返回 false)比阻塞更安全 - 不要在生产者线程里做格式化:时间戳、线程 ID、级别字符串全部由消费者线程统一生成,减少锁粒度和 CPU 波动
异步 IO 不等于用 std::async 包一层 write()
std::async(std::launch::async, [&]{ fs 只是把同步写挪到另一线程,没解决底层 write() 的系统调用阻塞问题,且频繁创建线程开销大。真正的异步需绕过 libc 缓冲,直通内核接口。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- Linux 用
io_uring(5.1+)或aio_write()(注意aio_suspend()阻塞问题) - Windows 用
WriteFile()+OVERLAPPED+ I/O 完成端口(IOCP) - 跨平台折中方案:用
std::thread+std::ofstream+std::ios_base::binary+fs.rdbuf()->pubsetbuf(nullptr, 0)关闭缓冲,再配合fs.flush()控制刷盘时机
文件滚动和路径处理最容易栽在 Windows 和 macOS 差异上
日志滚动时 rename / move 行为三端不一致:Linux rename() 原子,Windows MoveFileEx() 在目标存在时默认失败,macOS HFS+ 对硬链接支持弱。别手写跨平台 rename 逻辑。
- 用
std::filesystem::rename()(C++17),但它在 Windows 上对正在打开的文件会失败,需先fs.close()再 rename - 滚动前检查磁盘空间:用
std::filesystem::space(),但注意 macOS 返回值可能滞后,建议预留 50MB 缓冲 - 路径拼接统一用
std::filesystem::path,避免手动拼"log/" + name + ".log"—— Windows 反斜杠和 Linux 正斜杠混用会导致std::ofstream构造失败且无提示
真正难的不是并发模型,而是把时间戳精度、时区、文件编码(UTF-8 with BOM?)、信号安全(SIGUSR1 触发 flush)、以及 SIGPIPE 隐藏处理全揉进一个轻量头文件里 —— 这些细节堆起来,比选无锁队列或 IO 模型更耗神。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










