多线程不解决文件系统瓶颈,反而加剧竞争;应避免多线程直写同一文件,采用单写线程、mmap或分片文件解耦i/o,辅以o_cloexec和fsync等细节优化。

多线程本身不解决文件系统瓶颈,反而可能加剧它;关键在于避免多个线程直接竞争同一文件句柄或磁盘路径。
为什么多个线程同时写同一个文件会变慢
当多个线程调用 fwrite、ofstream::write 或 write() 写入同一个打开的文件描述符(或 fstream 对象)时,实际会发生:
- 内核级串行化:即使你没加锁,Linux 也会在
write()系统调用入口处对 fd 加内部锁,强制排队 - 缓冲区冲突:C++ 标准库流对象(如
ofstream)不是线程安全的,operator 可能破坏格式或导致部分写入 - 磁盘寻道放大:多个线程随机写不同偏移,触发大量磁头移动(HDD)或 NAND block 搬迁(SSD)
现象是 CPU 利用率低、iowait 升高、吞吐量不增反降。用 iotop -p <pid></pid> 或 perf record -e block:block_rq_issue 能确认是否卡在 I/O 队列。
std::shared_mutex 适合读多写少的配置文件场景
如果你的“文件系统操作”主要是并发读取(比如加载 JSON 配置、查 lookup 表),而写入极少(仅管理员更新),std::shared_mutex 是合理选择:
- 多个线程可同时持有
lock_shared(),零阻塞读取 - 写入线程调用
lock()时,会等待所有共享锁释放,并阻止新共享锁获取 - 注意:它保护的是内存中的缓存数据,不是文件本身 —— 你仍需在写入磁盘前用临时文件 +
rename()保证原子性
错误用法:std::shared_mutex 直接套在 ofstream 上 —— 它无法防止两个线程同时调用 fs 导致内容交错。
真正解耦 I/O 瓶颈的三种落地方式
不要让业务线程直接碰磁盘。把写操作剥离到专用线程或批量通道:
-
单写线程 + 无锁队列:所有工作线程将待写数据(如
std::string或结构体)推入moodycamel::ConcurrentQueue,由一个专属 I/O 线程顺序消费并刷盘 -
mmap + 原子偏移更新:对固定大小日志文件,用
mmap()映射整块内存,用std::atomic<size_t></size_t>管理当前写入位置,规避lseek()和锁 —— 但需确保写入不越界且跨页安全 -
分片文件 + 线程绑定:按线程 ID 将日志写入不同文件(
log_001.txt,log_002.txt…),彻底消除竞争;后续用cat或归并程序合并
这三种方式里,分片最简单、单写线程最通用、mmap 最高效但也最难做崩溃恢复 —— 选哪个取决于你能否接受丢失最后几条日志。
容易被忽略的底层细节
即便做了上述隔离,以下两点仍常被跳过:
- open() 时务必加
O_CLOEXEC:防止 fork 子进程后意外继承 fd,导致子进程退出时关闭父进程正在用的文件句柄 - 写入后调用
fsync()(非fflush()):fflush()只清 C 库缓冲区,fsync()才真正落盘;但频繁调用会拖慢性能,建议每 N 条或每 T ms 同步一次
磁盘不是内存,它的延迟是毫秒级的,而 CPU 是纳秒级的 —— 所有优化的本质,都是让线程别在这儿干等。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











