选错 Timer 类型是 C# 定时任务出问题的最常见原因:System.Threading.Timer 适用于后台无 UI 场景,回调在线程池线程;System.Timers.Timer 是其事件封装,仍需注意线程安全;System.Windows.Forms.Timer 仅限 WinForms UI 线程安全更新;.NET 6+ 推荐 PeriodicTimer 配合 await 使用。

选错 Timer 类型是 C# 定时任务出问题的最常见原因——不是代码写错了,而是用错了类。控制台程序里用了 System.Windows.Forms.Timer 会直接不触发;UI 线程里用 System.Threading.Timer 直接更新控件会抛异常;想 await 一个定时器却用了老式同步回调,结果卡死主线程。
System.Threading.Timer:后台任务、无 UI 场景的首选
它最轻量,纯线程池回调,不依赖任何 UI 框架,适合控制台、服务、Worker Service。但回调在非 UI 线程执行,不能直接碰 TextBox.Text 这类控件。
- 构造时传入
TimerCallback委托,必须捕获异常,否则错误静默吞掉、定时器“假死”:var timer = new Timer(_ => { try { DoWork(); } catch (Exception ex) { Console.WriteLine($"Error: {ex}"); } }, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5)); -
Change()可动态改间隔,但注意:第一个参数是下次触发的延迟(dueTime),不是从现在起算的偏移量;设为TimeSpan.Zero表示立即触发下一次 - 必须手动
Dispose(),推荐用using或显式释放,否则线程池资源泄漏
System.Timers.Timer:需要事件模型 + 自动重置的后台服务
本质是 System.Threading.Timer 的封装,提供 Elapsed 事件,支持 AutoReset 和 Enabled 属性,语义更清晰,但仍是线程池线程触发。
-
Elapsed事件处理器里不能直接访问 WinForms/WPF 控件,要走Control.Invoke或Dispatcher.Invoke -
Interval单位是毫秒(不是TimeSpan),设0无效,最小有效值通常是 1 -
Stop()和Enabled = false效果一样,但Stop()更明确;Dispose()后不能再调用Start()
System.Windows.Forms.Timer:WinForms UI 更新唯一安全的选择
它只在 UI 线程上触发 Tick 事件,所有控件操作天然线程安全,但仅限于 WinForms 应用,且精度低(实际间隔可能比设置值多 10–30ms)。
- 必须在创建它的窗体线程中使用,控制台或 WPF 里 new 出来也不会工作
-
Interval单位是毫秒,设太小(如 1)没意义,系统消息泵处理不过来 - 不要在
Tick里做耗时操作,否则 UI 卡顿;真要异步,请启动Task.Run并在完成后再用Invoke更新界面
.NET 6+ 推荐:PeriodicTimer 配合 await
如果你在 .NET 6 或更高版本,且逻辑本身支持异步(比如要发 HTTP 请求、读文件),PeriodicTimer 是目前最干净的方案——它不绑定线程,也不强制回调,而是配合 await foreach 使用。
- 它不替代老 Timer,而是为 async/await 场景设计,不能直接替换同步回调逻辑
- 必须搭配
async方法和IAsyncEnumerable使用:var timer = new PeriodicTimer(TimeSpan.FromSeconds(2)); while (await timer.WaitForNextTickAsync()) { await DoWorkAsync(); // 这里可以 await - 没有
Change()方法,要改间隔就得 dispose 重建新实例
真正容易被忽略的是:所有基于线程池的 Timer(Threading 和 Timers)在回调中如果发生未捕获异常,整个定时器就停摆了——它不会重试,也不会报错到控制台,只会静默终止。这点比 UI Timer 的“卡 UI”更难排查。










