WinForms中更新UI必须用System.Windows.Forms.Timer,因其在UI线程触发Tick事件;后台任务应选System.Threading.Timer,轻量且不阻塞UI;跨线程更新UI可选System.Timers.Timer并设置SynchronizingObject;.NET 6+推荐PeriodicTimer配合IHostedService管理生命周期。

别直接 new Timer() 就开干——选错类型,UI 会卡死、部署会失败、定时器会“静默消失”。
WinForms 里更新 Label 或 Button,必须用 System.Windows.Forms.Timer
它在 UI 线程上触发 Tick 事件,所以你能直接写 lblStatus.Text = "running",不会抛 InvalidOperationException: Cross-thread operation not valid。但代价是:如果 Tick 里做耗时操作(比如读文件、调 HTTP),整个窗体就假死——鼠标拖不动、按钮点不了、窗口无法重绘。
- Interval 最小值是 1 毫秒,设成 0 会直接抛
ArgumentException - 启停用
Start()/Stop(),或者设Enabled = true/false;别靠改Interval来停,那不是设计意图 - 不要在
Tick里调Thread.Sleep()或长时间同步 IO;真要等,用await Task.Delay()+ 改用async void Tick(需 .NET 4.5+ 且小心重入) - 设计器拖进来和代码 new 效果一样,但设计器生成的字段名默认叫
timer1,容易跟别的 timer 混,建议重命名
后台任务(如日志轮转、心跳上报),优先用 System.Threading.Timer
它不依赖任何 UI 框架,纯线程池回调,轻量、无状态、不占 UI 线程。但这也意味着:它根本不知道窗体在哪,更不能碰 TextBox.Text——一碰就崩。
- 构造时
period设成Timeout.Infinite或 0,它就只执行一次;要周期运行,必须确保period > 0,比如TimeSpan.FromSeconds(5) - 回调里任何未捕获异常都会被吞掉,定时器照常跑,但后续逻辑全丢——务必包
try/catch,至少打日志 - 它不持有自身引用,如果变量是局部的(比如写在
Main()里没存到字段),GC 可能回收它导致“启动了却没反应” - 用完记得
Dispose(),或套using;长期运行的服务里,建议存为类字段并手动管理生命周期
需要事件模型又想跨线程更新 UI?System.Timers.Timer 加 SynchronizingObject
它本质是 System.Threading.Timer 的封装,提供 Elapsed 事件,比纯回调更符合 C# 习惯。关键点在于:SynchronizingObject 属性可以指定一个实现了 ISynchronizeInvoke 的对象(比如 WinForms 的 Form 或 Control),让 Elapsed 回调自动封送到 UI 线程。
- 不设
SynchronizingObject时,Elapsed在线程池线程跑,和System.Threading.Timer行为一致 - 设了之后,它内部调用
BeginInvoke,你就能在事件处理里安全写label.Text = "..."; -
AutoReset = false时,只触发一次;设为true(默认)才循环,这点比System.Threading.Timer直观 - 别在
Elapsed里直接timer.Stop()——可能引发“正在触发事件时关闭”的竞态;应先设AutoReset = false,再在事件末尾Stop()
.NET 6+ 新项目,考虑 PeriodicTimer + IHostedService
如果你在写 ASP.NET Core 后台服务或控制台宿主服务,System.Threading.Timer 虽然能用,但生命周期难对齐——服务停了,timer 还在跑。而 PeriodicTimer 是 awaitable 的,配合 IHostedService.StartAsync/StopAsync 更自然。
-
PeriodicTimer不是“控件”,没有事件,只有WaitForNextTickAsync(),适合写在while (await timer.WaitForNextTickAsync()) { ... }循环里 - 它不自动捕获异常,也不重试,出错就得自己处理,否则循环中断
- 必须手动
Dispose(),否则资源泄漏;在StopAsync里调timer.Dispose()是标准做法 - 它不兼容 .NET Framework 或 .NET Core 3.1 及更早版本,老项目别硬切
最易被忽略的一点:Timer 不是“只要 new 出来就一定在跑”。它的行为高度依赖上下文——UI 线程、线程池、宿主生命周期、异常处理粒度。写完别只测“第一次触发”,一定要验证“连续跑 5 分钟是否漏 tick”“异常后是否还能恢复”“服务关闭时是否干净退出”。










