应选对 timer 类型:winforms ui 更新必须用 system.windows.forms.timer,后台任务优先 system.threading.timer,.net 6+ 推荐 periodictimer;错用会导致卡死、泄漏或崩溃。

别用 Task.Delay 模拟周期性任务,除非你明确控制取消、异常和并发;真正需要定时触发的场景,必须选对 Timer 类型——错一个,轻则 UI 卡死、更新失败,重则内存泄漏、部署崩溃。
WinForms 里更新 Label 或 Button,只能用 System.Windows.Forms.Timer
它不是“可选项”,是唯一线程安全的 UI 定时器。Tick 事件天然在 UI 线程执行,label.Text = "ok" 这种写法不会抛异常,也不需要 Invoke。
- 必须在窗体已显示(
Show()或ShowDialog()后)且消息泵正常运行时才有效;窗体没 Show、或主线程被Thread.Sleep卡住,Tick 就永远不会来 -
Interval单位是毫秒,但实际精度受系统计时器分辨率限制,设1或5毫秒毫无意义,常见有效下限是15–50ms - Tick 里禁止耗时操作:HTTP 请求、文件读写、
Thread.Sleep都会导致界面假死,后续 Tick 延迟甚至堆积 - 关闭窗体前务必调用
timer.Stop()或Dispose(),否则可能残留引用阻碍 GC
后台轮询、心跳、日志清理,优先选 System.Threading.Timer
它是 .NET 最轻量、性能最高的定时器,回调直接走线程池,无事件封装、无额外对象分配,适合高频(如每 20ms 检查一次状态)或长期运行的服务。
- 构造时
period必须 > 0,设成Timeout.Infinite或0会导致只触发一次 - 回调函数内必须
try/catch:未捕获异常不会终止定时器,但会静默丢弃后续调度,看起来像“断连” - 不能直接访问任何 WinForms 控件,否则抛
InvalidOperationException: Cross-thread operation not valid - 必须显式
Dispose(),用using最稳妥;若回调闭包捕获了窗体实例,不释放会导致内存泄漏
Task.Delay + while 循环不是 Timer 替代品,只是“可控延时”
它本质是异步等待,没有内置周期管理、无启用/暂停语义、也不提供调度上下文。真要用,得自己补全三件事:取消、异常传播、防重入。
- 必须传入
CancellationToken,并在每次await Task.Delay(..., token)中传递,否则无法响应停止请求 - 循环体内的
DoWork()若抛异常,整个async void方法就崩了,且异常无法被外层捕获 - 如果
DoWork()是异步的,且没加锁或标记,可能多个迭代并发执行——比如上一轮还没结束,下一轮又进来了 - 它不适合替代 UI 刷新类需求:没有
Tick的语义,也无法与窗体生命周期绑定
.NET 6+ 新项目,PeriodicTimer 是更现代的选择
它专为 await 设计,支持自然的取消语义,回调在 await 点挂起,不抢占线程池线程,也不存在“回调中抛异常导致定时器失联”的问题。
- 必须搭配
await foreach使用:await foreach (var _ in timer),不能手动调用 - 它本身不提供 UI 线程切换能力,更新控件仍需
this.Invoke(...)或Dispatcher.Invoke(...) - 不兼容 .NET 5 及更早版本,老项目升级前要确认目标框架
- 首次触发时机由
WaitForNextTickAsync控制,比传统 Timer 更易做精确节流
最常被忽略的点:Timer 的“生命周期”不是靠 Enabled = false 或 Stop() 就算结束的。System.Threading.Timer 和 System.Timers.Timer 的底层句柄只有 Dispose() 才真正释放;而 System.Windows.Forms.Timer 如果在非 UI 线程创建,哪怕调了 Start(),也永远不会 Tick——它压根没机会接入消息泵。











