File.ReadAllText 难监控因其调用链隐藏于 BCL 底层,不抛异常、不暴露句柄、无事件可拦截;实际耗时(如 NTFS 元数据锁争用)无法被 Stopwatch 准确捕获,需改用 FileStream + StreamReader 手动构造以支持 DiagnosticSource 钩子。

为什么 File.ReadAllText 看似简单却最难监控
因为它的调用链完全隐藏在 BCL 底层,不抛异常、不暴露句柄、不触发可拦截的事件。你看到日志里“读取 config.json 成功”,但不知道它实际花了 800ms——其中 790ms 耗在 NTFS 元数据锁争用上。
- 别依赖
Stopwatch包裹调用:它测不准内核态等待时间(比如磁盘队列积压) - 优先改用
FileStream+StreamReader手动构造,才能插入DiagnosticSource钩子 - 注意 .NET 6+ 的
File.ReadAllTextAsync默认不支持CancellationToken超时中断,得自己套一层Task.WaitAsync
如何让 FileSystemWatcher 不漏事件也不狂打日志
它本质是 Win32 ReadDirectoryChangesW 的封装,事件丢失和重复是常态,不是 bug。
- 必须设置
EnableRaisingEvents = true后再订阅Changed事件,顺序反了就收不到首批变更 -
NotifyFilter别滥用NotifyFilters.LastWrite | NotifyFilters.FileName—— 某些 NAS 设备会把一次保存拆成 3 次写入,触发 3 次事件 - 日志采样建议用
ConcurrentDictionary<string long></string>缓存路径最近触发时间,同路径 1 秒内只记首条
IOThreadPool 线程饥饿导致文件操作批量超时
.NET 5+ 把文件 IO 默认调度到专用线程池(IOThreadPool),但它默认只有 MinThreads = 1,并发高时直接卡死。
- 检查
ThreadPool.GetMinThreads(out _, out ioThreads),如果ioThreads是 1,立刻调ThreadPool.SetMinThreads(4, 4) - 不要在
async void方法里做File.Copy——异常会吞掉,且无法参与IOThreadPool调度控制 - 大文件复制务必用
CopyOptions.KeepProperties | CopyOptions.FailIfDestinationExists,避免属性同步阻塞线程
用 ActivitySource 补全文件操作的分布式追踪断点
默认情况下,FileStream 不产生任何 Activity,Open/Read/Close 全是黑盒。
- 需手动创建
ActivitySource并在FileStream构造前后 Start/Stop Activity,名称建议带操作类型,如"file.open"、"file.read.blocking" - 把
FileStream.SafeFileHandle.DangerousGetHandle()作为Tag上报,便于后续关联 ETW 或 ProcMon 日志 - 警惕
FileStream的leaveOpen: true参数:若上游Activity已结束,下游StreamReader的读取将无法挂载到原 TraceId
PerfView 里 Microsoft-Windows-Kernel-IO 事件的 IoPriority 字段。









