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.Text或
Control.Invoke会抛出
InvalidOperationException: “线程间操作无效”。这不是“定时器不安全”,而是 WinForms/WPF 的线程亲和性限制。
安全做法取决于场景:
WinForms 中用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并加超时控制;否则线程池饥饿、定时漂移、甚至整个应用卡顿都会找上门。
