原子创建文件必须用系统级原语:linux/macos 用 open() 配合 o_creat | o_excl | o_wronly,windows 用 createfile() 配合 create_new;fopen 和 std::ofstream 不具备原子性,预检 exists 后创建必然存在竞态窗口。

用 fopen 或 std::ofstream 直接创建文件不原子
多线程下直接调用 fopen("file.txt", "w") 或 std::ofstream("file.txt"),本质是先检查文件是否存在、再 truncate 或覆盖写入——中间有竞态窗口。两个线程几乎同时执行,可能都判断“文件不存在”,都成功打开并写入,最终只剩一个内容,另一个被静默覆盖。
实操建议:
- 必须用系统级原子创建原语,比如 Linux 的
open()配合O_CREAT | O_EXCL标志 - C++ 标准库没有跨平台原子创建接口,
std::filesystem::create_directories不解决文件级竞态,它只建目录 - Windows 下对应的是
CreateFile带CREATE_NEW标志,不是CREATE_ALWAYS
open() + O_EXCL 是最可靠方案(Linux/macOS)
这是 POSIX 定义的原子行为:仅当文件**完全不存在**时才创建成功,否则返回 EEXIST 错误。内核保证该判断和创建在单次系统调用中完成,无竞态。
实操建议:
- 不要用
fopen封装,必须用open()系统调用(#include <fcntl.h></fcntl.h>和#include <unistd.h></unistd.h>) - 必须同时指定
O_CREAT | O_EXCL | O_WRONLY,缺一不可;只加O_CREAT仍会覆盖已有文件 - 创建成功后,用
fdopen()转成FILE*再用fwrite等,或直接用write() - 注意 umask 影响权限,显式传
0644比依赖默认更可控
示例关键片段:
int fd = open("lockfile", O_CREAT | O_EXCL | O_WRONLY, 0644);<br>if (fd == -1) {<br> if (errno == EEXIST) { /* 其他线程已抢到 */ }<br> else { /* 真错,如磁盘满 */ }<br>}
Windows 下必须用 CreateFile + CREATE_NEW
Windows 的 CreateFile 中,CREATE_ALWAYS 会截断已有文件,OPEN_ALWAYS 会打开或创建,都不满足原子性要求。只有 CREATE_NEW 在文件存在时失败,且创建动作本身由内核原子完成。
实操建议:
- 不能用 CRT 的
fopen,必须调用 Win32 API(#include <windows.h></windows.h>) - 第三个参数(
dwShareMode)建议设为0,避免其他进程/线程同时打开干扰判断 - 第四个参数(
lpSecurityAttributes)通常传NULL,除非需要继承句柄 - 创建成功后得到
HANDLE,可用_open_osfhandle转为 C 文件描述符,再用_fdopen得FILE*
跨平台封装要小心临时文件与重命名陷阱
有人想用 mkstemp(Linux)或 GetTempFileName(Windows)生成唯一临时名,再 rename 到目标路径——但 rename 在不同文件系统间不原子,且 Windows 上对已存在目标文件的 rename 行为与 POSIX 不一致(可能成功覆盖)。
实操建议:
- 避免“生成临时名 → 写内容 → rename”流程,除非你严格控制所有路径在同一挂载点/卷上
- 若必须跨平台,优先在编译期分条件:Linux/macOS 用
open+O_EXCL,Windows 用CreateFile+CREATE_NEW - 别依赖
std::filesystem::exists做预检——它和后续创建之间必然存在竞态窗口,纯属误导 - 错误处理要区分“已存在”(预期竞态)和“权限不足/磁盘满”(真错误),前者应快速重试或让步
事情说清了就结束。真正的难点从来不是“怎么写”,而是意识到 O_EXCL 和 CREATE_NEW 这两个标志不可替代——所有绕开它们的方案,都在赌竞态不会发生。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











