应选system.threading.timer做后台任务:它最轻量、回调在线程池执行、需手动异常处理和dispose防泄漏;period=0表示单次执行而非禁用;回调必须try/catch且不可在其中直接dispose,须用change()配合状态标记并主线程释放。

别用 System.Windows.Forms.Timer 做后台任务,它只在 UI 线程跑,一卡全卡;真要定时执行耗时操作(比如发 HTTP、读文件、查数据库),必须选 System.Threading.Timer 或 System.Timers.Timer,但二者行为差异极大——选错就丢触发、漏回调、线程炸锅。
System.Threading.Timer 怎么初始化才不会“只执行一次”
它默认不自动重复,很多人写了 new Timer(cb, null, 1000, 0) 却发现只跑了一次,其实是把 period 设成了 0 或 Timeout.Infinite。记住:period = 0 表示“单次执行”,不是“禁用”。
-
TimeSpan.Zero作为dueTime→ 立即首次触发 -
TimeSpan.FromSeconds(5)作为period→ 每 5 秒重复一次 -
Timeout.Infinite作为period→ 只执行首次,之后停住 - 构造后 timer 就开始计时,没有 Start() 方法
- 务必把 timer 实例存为类字段(如
private Timer _timer),否则局部变量一退出作用域,GC 可能回收它,任务悄无声息终止
回调里抛异常会导致后续调度静默失败
System.Threading.Timer 的回调运行在线程池线程上,未捕获异常不会中断定时器,但会直接丢弃本次回调,且不再继续调度——看起来像“卡住”或“断连”,实际是崩了没报错。
- 必须在回调函数内显式
try/catch,否则你看不到任何错误信息 - 不要依赖全局异常处理器(如
AppDomain.UnhandledException),它收不到线程池异常 - 异常处理完建议打日志,至少写进
Console.WriteLine或ILogger - 如果任务本身可能失败(如网络超时),应在 catch 后决定是否重试,而不是任由 timer 继续空转
怎么安全停止并释放 System.Threading.Timer
不能在回调里直接调 timer.Dispose(),底层句柄可能正被使用,导致 ObjectDisposedException 或静默失败;也不能只调 Dispose() 就完事,因为正在执行的回调可能访问已释放资源。
- 先调
timer.Change(Timeout.Infinite, Timeout.Infinite),立刻停掉后续所有触发 - 用
Interlocked或volatile bool标记“已取消”,并在回调开头检查该标记 - 在主线程(如窗体关闭、服务停用时)再调
timer.Dispose() - 别把 timer 声明为
static后忘了 Dispose,长期运行的服务里这会造成内存泄漏
最易被忽略的一点:Timer 回调和 UI 更新是两回事。哪怕你在 WinForms 里用了 System.Threading.Timer,也绝不能直接给 label.Text 赋值——必须走 label.Invoke 或 Control.BeginInvoke。跨线程访问控件不是“偶尔出错”,而是每次必崩。











