Stopwatch.StartNew() 是唯一推荐的初始化方式,它自动完成实例化和启动,避免因漏调 Start() 导致 ElapsedMilliseconds 恒为 0;应优先使用 Elapsed.TotalMilliseconds 获取高精度耗时,微基准测试中禁用截断精度的 ElapsedMilliseconds。

Stopwatch.StartNew() 是唯一推荐的初始化方式
别写 new Stopwatch() 再手动调用 Start(),漏掉 Start() 就会导致 ElapsedMilliseconds 永远是 0。而 Stopwatch.StartNew() 是静态工厂方法,内部自动完成实例化 + 启动,语义明确、零遗漏风险。
常见错误现象:stopwatch.Elapsed.TotalMilliseconds 始终返回 0;调试时发现 IsRunning 为 false,但代码里根本没调 Stop() —— 其实就是压根没 Start()。
注意事项:
-
StartNew()返回的是已启动状态的实例,后续可直接Stop() - 它会自动检测系统是否支持高精度计数器(
QueryPerformanceCounter),不需你手动判断IsHighResolution - 在 .NET Framework 4.5+ 和所有现代 .NET Core / .NET 5+ 中行为一致
Stopwatch.ElapsedMilliseconds vs Elapsed.TotalMilliseconds
这两个属性看着像,但精度和用途完全不同:ElapsedMilliseconds 是 long 类型,只保留毫秒整数部分,小数全被截断;Elapsed.TotalMilliseconds 是 double,保留全部精度(含微秒、纳秒换算后的余数)。
典型误用场景:做微基准测试(micro-benchmark)时用 ElapsedMilliseconds 比较两次执行差异,结果全是 0 或跳变剧烈——因为真实耗时可能只有 0.3ms,直接被抹成 0。
正确做法:
- 需要判断“是否超 10ms”这类阈值检查,用
ElapsedMilliseconds足够 - 做性能对比、计算平均值、记录日志精度要求高时,必须用
Elapsed.TotalMilliseconds(建议格式化为F6) - 想拿到原始计时器刻度(比如做底层校准),用
ElapsedTicks,它不受系统时钟漂移影响
Stopwatch 在 async/await 场景下怎么不出错
Stopwatch 本身不是线程安全的,但它真正的雷区不在并发,而在 await 切换线程后继续 Stop() —— Start() 和 Stop() 不在同一线程,逻辑就乱了(不会抛异常,但计时不准确)。
错误写法:
var sw = Stopwatch.StartNew(); await Task.Delay(100); sw.Stop(); // ❌ await 后可能已切换线程
正确写法:
- 把
Start()和Stop()锁在同一同步上下文内,例如都在一个async方法开头和结尾,中间不跨方法边界 - 如果必须分段测(比如只测 IO 阶段),改用
Environment.TickCount64手动差值,但要注意它约 24.9 天会溢出;此时Stopwatch.IsHighResolution可能为false,精度回落到DateTime.UtcNow级别 - 绝不把
Stopwatch实例传进异步 lambda 或后台任务中再调Start()/Stop()
重复使用 Stopwatch 实例比反复 new 更高效
有人在循环里写 var sw = Stopwatch.StartNew(); sw.Stop();,短时间看不出问题,但频繁分配对象会抬高 GC 压力,尤其在 .NET Framework 或低配设备上可能拖慢整体吞吐。
复用方案:
- 用
Restart()替代Stop()+Start():它原子性地停止 + 重置 + 启动,适用于连续多次测量同一类操作 - 用
Reset()+Start()组合,适合需要显式控制启停节奏的场景(如 UI 倒计时暂停/恢复) - 注意:
Restart()是 .NET Framework 4.5+ 才支持,旧版本只能手动Reset()+Start()
真正容易被忽略的一点:Stopwatch 的精度依赖硬件和 OS 支持。若 Stopwatch.IsHighResolution 返回 false,说明系统 fallback 到了低精度计时器,此时哪怕用了 StartNew(),误差也可能达 10ms 以上——这对微基准测试是致命的,但多数业务逻辑感知不到。











