跨线程复用 std::ofstream 必须配外部同步,因标准未保证其线程安全,直接让多个线程调用同一对象的 operator<< 等成员函数会导致未定义行为。

std::ofstream 跨线程复用必须配外部同步
直接让多个线程调用同一个 std::ofstream 对象的 operator 是未定义行为 —— C++ 标准明确禁止。哪怕只是读取状态(如 <code>is_open()),只要存在任意线程在写,就构成数据竞争。
- 正确做法:共享一个
std::ofstream实例 + 全局或类内mutable std::mutex,每次完整写入(含格式化、\n、flush())都包在std::lock_guard里 - 错误做法:每个线程自己
std::ofstream file("log.txt", std::ios::app)—— 在 Windows 上大概率触发Access is denied,Linux 下虽能打开但写入偏移量不可控,导致内容覆盖或乱序 - 注意
std::ios::app不等于线程安全:它只保证每次系统级write()追加到文件末尾,但 C++ 流的缓冲区拼接、格式化、多次write()调用仍需锁保护
文件句柄泄漏常因异常绕过 fclose / close
裸用 fopen() + fclose() 或 open() + close() 时,若中间抛出异常,句柄不会自动释放。操作系统对每个进程的句柄数有限制(Linux 默认 1024,Windows 通常 16K),泄漏积累后会触发 Too many open files 错误。
- 优先用 RAII 封装:比如自定义
FileHandle类,在构造中open(),析构中close();或直接用std::fstream(其析构自动关闭) - 避免在锁内做可能抛异常的操作:例如在
std::lock_guard作用域里调用JSON::serialize(),一旦抛异常,锁虽会释放,但句柄若在锁外打开、锁内使用,就容易漏关 - 检查返回值:用
open()时判断是否返回-1,用fopen()判断是否为nullptr,否则后续write()或fwrite()会触发段错误或未定义行为
std::mutex 不能保护系统级文件偏移量竞争
即使你用 std::mutex 把所有 C++ 流操作都串行化了,如果底层混用了系统调用(如 lseek() + write())或跨进程访问同一文件,依然可能出问题 —— 因为 std::mutex 只管本进程内的线程同步,不干涉内核维护的文件描述符偏移量。
- 典型踩坑:一个线程用
std::ofstream写,另一个线程用write(fd, ...)往同一文件写 → 偏移量互相覆盖,输出错位 - 解决路径一:统一抽象层,全部走 C++ 流或全部走系统调用,不要混用
- 解决路径二:需要混用时,用
dup()复制描述符并独立控制偏移量;或改用pwrite()(POSIX)替代lseek()+write(),后者是原子的 - Windows 用户注意:
CreateFile()中的FILE_SHARE_WRITE仅控制“能否同时打开”,不解决并发写冲突;真正防覆盖得靠LockFileEx()
日志场景下 flush() 和 fsync() 的取舍
log_file.flush() 只把 C++ 缓冲区刷到 OS 缓冲区,不保证落盘;而 fsync()(POSIX)或 FlushFileBuffers()(Windows)强制刷盘,但代价高(毫秒级延迟),频繁调用会拖垮吞吐。
立即学习“C++免费学习笔记(深入)”;
- 普通日志:用
flush()+std::ios::app即可,崩溃时最多丢最后一小段 - 关键事务日志(如数据库 WAL):必须在
flush()后立刻跟fsync(),且该调用必须和写操作在同一锁保护下,否则其他线程可能在flush和fsync之间插入写入 - 不要依赖
std::endl:它等价于\n + flush,但 flush 频次过高;日常用"\n",按需在关键点手动flush()


















