c# System.Threading.Timer 和 System.Timers.Timer 的区别和线程安全

System.Threading.Timer首次回调立即执行,System.Timers.Timer首次触发需等待Interval;两者均使用线程池且不保证线程安全,回调并发执行可能引发竞态;UI更新须显式切换主线程,Dispose()为唯一可靠释放方式。

System.Threading.Timer 和 System.Timers.Timer 的触发时机与线程模型差异

两者都用线程池线程执行回调,但触发逻辑根本不同:System.Threading.Timer 默认「立即执行首次回调(可设 dueTime = 0)」,之后按 period 间隔重复;而 System.Timers.Timer 总是「先等一个 Interval 才触发第一次 Elapsed 事件」,哪怕你刚调用 Start()。这意味着如果你需要「启动即干活」,Threading.Timer 更直接,不用额外手动调一次回调。

线程模型上,它们都**不保证线程安全**——回调本身在线程池中并发执行,但二者对「重入」的默认行为不同:

  • System.Threading.Timer:每次回调都是独立的线程池任务,若回调执行慢、且 period 小于执行耗时,会堆积多个并行回调,可能引发竞态(比如同时写同一个 Dictionary
  • System.Timers.Timer:同样不阻塞后续触发,AutoReset = true(默认)时,下一次 Elapsed 会在前一次还没结束时照常触发,也会并发执行

为什么不能直接在回调里更新 UI?怎么安全地切回主线程?

两者回调都在后台线程运行,直接访问 TextBox.TextControl.Invoke 会抛出 InvalidOperationException: “线程间操作无效”。这不是“定时器不安全”,而是 WinForms/WPF 的线程亲和性限制。

安全做法取决于场景:

  • WinForm

    s 中用 System.Timers.Timer:可设置 SynchronizingObject = this(或任意 ISynchronizeInvoke 对象),它会自动把 Elapsed 事件封送到 UI 线程 —— 这是它比 Threading.Timer 唯一方便的地方
  • 通用方案(尤其控制台、服务、或跨平台):用 Task.Run + await Dispatcher.InvokeAsync(...)(WPF)或 this.Invoke((MethodInvoker)delegate { ... })(WinForms)显式切换
  • 千万别在回调里直接 new Form 或 ShowDialog() —— 即使切了线程,模态对话框仍可能卡死消息循环

内存、精度、Dispose:三个最容易被忽略的坑

实测数据显示:System.Threading.Timer 实例几乎零内存分配(Allocated = 0 B),而 System.Timers.Timer 每个实例固定占用约 18 KB 内存(.NET Framework 4.8+)。高频创建/销毁大量定时器时,后者会明显推高 GC 压力。

精度方面:Threading.Timer 首次触发延迟更稳定(实测平均 ~15 ms),Timers.Timer 因事件路由开销,首次延迟波动大(实测达 90 ms 以上)。

最关键的是资源释放:

  • System.Threading.Timer 必须显式调用 Dispose(),否则回调可能持续执行(即使引用丢失),造成内存泄漏和意外触发
  • System.Timers.Timer 同样必须 Dispose(),且建议配合 Stop() 使用;若只 Stop()Dispose(),内部事件订阅和线程池句柄不会释放
  • 别依赖析构函数 —— 它们都不实现终结器,Dispose() 是唯一可靠方式

选哪个?看这三句话就足够

System.Threading.Timer 当你:需要极致轻量、要精确控制首次触发时机、写后台服务/高性能中间件、能接受回调是纯委托(不带事件语义)。

System.Timers.Timer 当你:正在 WinForms 项目中且想省掉手动线程切换、需要 AutoReset/Enabled 这类状态属性、团队习惯事件编程模型、不介意多那 18 KB。

永远别用它们做耗时操作 —— 无论是读文件、发 HTTP 请求还是复杂计算,都应外包给 Task.Run 并加超时控制;否则线程池饥饿、定时漂移、甚至整个应用卡顿都会找*。