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

来源:这里教程网 时间:2026-02-21 17:39:02 作者:

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
并加超时控制;否则线程池饥饿、定时漂移、甚至整个应用卡顿都会找上门。

相关推荐