必须自主控制io队列,因windows默认线程池无法应对突发写入,易引发文件访问冲突、句柄争抢与系统级共享违例;应采用concurrentqueue+单后台线程节流,合理设置filestream缓冲区与异步选项,并优先使用同卷原子替换、权限预检及io监控降级策略。

IO 队列必须自己控,ThreadPool 默认调度扛不住突发写入
Windows 默认线程池对短时密集 File.WriteAllText 或 FileStream.Write 完全不设防,尤其在日志批量刷盘、上传文件解压、监控轮询等场景下,几十个并发写操作可能瞬间拉起上百线程,争抢磁盘句柄和缓冲区,导致 IOException: The process cannot access the file because it is being used by another process 或系统级 STATUS_SHARING_VIOLATION。
实操建议:
- 禁用直接调用
Task.Run(() => File.WriteAllBytes(...))这类“看起来异步实则乱扔”的写法 - 用
ConcurrentQueue<action></action>+ 单后台线程(非Task.Run)做写入节流,吞吐可控且无资源竞争 - 若需保序,队列元素带
TaskCompletionSource<bool></bool>,写完再SetResult(true),避免上层盲目await Task.Delay - 别依赖
async/await自动缓解 IO 压力——它只释放线程,不减少实际磁盘请求频次
FileStream 缓冲区大小不是越大越好,尤其小文件高频写
默认 FileStream 缓冲区是 4KB,有人改成 1MB 想“提升性能”,结果在每秒数百个 2KB 日志条目场景下,反而因频繁触发 Flush() 和内存拷贝,CPU 占用翻倍、延迟毛刺明显。
实操建议:
- 小文件(50 次/秒),缓冲区设为
4096或8192,匹配 NTFS 簇大小 - 大文件顺序写(如视频分片),可设为
65536,但必须配合FileOptions.WriteThrough | FileOptions.SequentialScan - 永远显式传入
useAsync: true(即new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous)),否则WriteAsync会退化为同步阻塞 - 别复用同一个
FileStream跨长时间——句柄泄漏风险高,且 Windows 文件系统对长时打开句柄有内部锁开销
临时文件 + 原子替换比直接覆盖更稳,但要注意 MoveTo 的权限陷阱
直接 File.WriteAllText(path, content) 在写入中途崩溃,会导致目标文件损坏或截断;而用 File.WriteAllText(tempPath, content); File.Move(tempPath, path) 能保证原子性,但 File.Move 在跨卷、NTFS 权限受限或防病毒软件拦截时会静默失败,抛出 UnauthorizedAccessException 或卡住数秒。
实操建议:
- 临时路径必须和目标路径同盘符(用
Path.GetPathRoot(target)校验),否则MoveTo变成复制+删除,失去原子性 - 提前检查目标目录写权限:
new FileInfo(targetDir).Directory?.GetAccessControl().GetAccessRules(true, true, typeof(SecurityIdentifier)),比试错更可靠 - 防杀软干扰:临时文件名避开
*.tmp、*.log等敏感后缀,改用带时间戳哈希的随机名,如log_20240521_7f3a9b.tmpdata - 替换失败时,保留临时文件并记录完整路径——比删掉后重试更容易定位是磁盘满还是权限问题
监控不能只看 CPU 和内存,IOReadBytesPerSec 和 Handle Count 才是风暴前兆
系统看似空闲,但 Process.IOReadBytesPerSec 持续 >50MB/s 或句柄数突破 5000,往往意味着文件操作已失控。此时 dotnet-counters 或 PerfMon 中的 .NET CLR Memory/# of Pinned Objects 也常同步飙升——大量 byte[] 被 GC pinned 导致内存碎片。
实操建议:
- 在启动时注册
AppDomain.CurrentDomain.ProcessExit和Console.CancelKeyPress,强制清空 IO 队列并WaitAll当前写任务,避免进程退出时丢数据 - 用
PerformanceCounter每 2 秒采样一次IODataBytesPerSec,超阈值(如 30MB/s 持续 5 秒)就自动降级:暂停非关键写入、切到内存缓冲、发告警 - 别信“磁盘足够快就不用控速”——NVMe 盘的随机写 IOPS 上限仍是硬约束,且系统缓存压力会传导到内存和 pagefile
真正难的不是写几行节流代码,而是得想清楚哪些操作可以合并、哪些必须保序、哪些失败能容忍——这些决策点藏在业务逻辑里,没法靠通用库兜底。










