原子写入需先写临时文件再原子替换:windows用file.replace,linux/macos用file.move(同卷),失败时回退到读-写-删;须配状态标记与异常清理。

崩溃后文件写入丢失,本质是没做原子性保障
程序崩溃时正在写文件,轻则数据不全,重则损坏已有内容——这不是运气问题,是没把“写入完成”和“文件可见”真正分开。C# 默认的 File.WriteAllText 或 StreamWriter 直接覆写原文件,一旦中途退出,原文件就没了。
核心思路:先写到临时文件,再用原子操作替换原文件。Windows 上靠 File.Replace,Linux/macOS 靠 File.Move(需同文件系统),两者都依赖底层 rename 的原子性。
- 必须确保临时文件和目标文件在同一个卷(否则
File.Replace会失败并抛出IOException) - 临时文件路径建议用
Path.GetTempFileName(),但注意它生成的文件在系统临时目录,跨卷风险高;更稳妥的是用Path.Combine(Path.GetDirectoryName(targetPath), Guid.NewGuid() + ".tmp") - 写完临时文件后,务必调用
File.Flush()和stream.Close()(或用using),否则操作系统可能还缓存着数据
File.Replace 是 Windows 下最可靠的恢复基底
File.Replace 不仅移动文件,还能自动备份旧文件(可选),且整个操作由 NTFS/exFAT 文件系统保证原子性:要么全成功,要么原文件完好无损。这是比手动 Delete + Move 安全得多的选择。
典型误用是忽略第三个参数 ignoreMetadataErrors:如果目标文件被其他进程锁定(比如被记事本打开),File.Replace 默认抛 IOException,而不是静默失败。
- 调用示例:
File.Replace(tempPath, targetPath, backupPath, false)—— 第四个参数false表示不忽略元数据错误,便于早期暴露权限或锁冲突 - 备份文件
backupPath可设为null,但建议指定(如targetPath + ".bak"),崩溃后可人工恢复 - 若
File.Replace抛异常,临时文件仍存在,可作为恢复依据;务必在 catch 块里清理它,否则磁盘会被碎片占满
跨平台时别硬套 File.Replace,改用 Move + 权限兜底
.NET 6+ 在非 Windows 系统上,File.Replace 内部退化为 File.Move + 删除,但前提是源和目标必须在同一文件系统。跨挂载点(比如 /home 和 /mnt/usb)会直接失败。
这时候不能只依赖 Move,得加一层防御:先检查 Directory.GetDirectoryRoot(source) 是否等于 Directory.GetDirectoryRoot(destination),不等就回退到「读-写-删」三步法(牺牲原子性,但保数据)。
- 检查是否同卷:
string rootA = Path.GetPathRoot(src); string rootB = Path.GetPathRoot(dst); if (rootA != rootB) { /* 回退方案 */ } - 回退方案中,新文件写完后,用
File.Copy(dst, backupPath, true)备份旧文件,再File.Delete(dst),最后File.Move(tempPath, dst) - 所有文件操作都要包裹
try/catch(IOException ex),尤其Delete和Move在 Linux 上可能因权限或占用失败
崩溃恢复不只是写入逻辑,还得留证据
光保证单次写入安全不够。如果程序在 File.Replace 后、删除临时文件前崩溃,下次启动时磁盘上会残留一个孤立的 .tmp 文件——它可能是成功的,也可能是失败的。没有标记,就无法判断。
解决方案很简单:写临时文件前,先写一个轻量级状态标记(比如 JSON 文件),记录“开始写入”;File.Replace 成功后,再更新为“已提交”。启动时扫描临时文件 + 对应标记,就能决定是继续提交还是丢弃。
- 标记文件名建议和临时文件强关联,例如
tempPath + ".state",内容只需一行:{"status":"committed"} - 不要用时间戳或进程 ID 做判断依据——它们不可靠,重启后失效
- 标记文件也要用同样原子方式写(先写
.state.tmp,再Replace),否则标记本身又成单点故障
事情说清了就结束。真正难的不是写出原子写入,而是想清楚:崩溃后,哪些中间态必须可识别、可决策、可清理。临时文件、备份文件、状态标记——每个都得有明确的生命周期和清理路径,否则越写越乱。










