System.Threading.Timer需手动管理生命周期、异常和线程安全:period=0为单次执行;回调中禁用直接Dispose(),须用Change()配合状态标记;state对象需线程安全;回调必须try/catch且不可依赖全局异常处理。

System.Threading.Timer 不是“设好就循环”的傻瓜定时器,它一创建就开始倒计时,回调在线程池线程跑,不处理异常就静默失败,不手动释放会泄漏资源——用错参数、漏捕异常、乱调 Dispose() 是三大高频翻车点。
构造时 period=0 不是禁用,而是单次执行
很多人传 period=0 想“先停着”,结果发现回调只执行一次就没了。这不是 bug,是设计:它等价于“延迟触发一次,之后不再重复”。
-
new Timer(cb, null, 1000, 0)→ 1 秒后执行一次cb,然后自动停止 -
new Timer(cb, null, 1000, 2000)→ 1 秒后首次执行,之后每 2 秒执行一次 -
new Timer(cb, null, Timeout.Infinite, Timeout.Infinite)→ 完全不触发(需后续调用Change()启动)
回调里不能直接调 Dispose(),必须用 Change() + 状态标记
在 TimerCallback 内部直接调 timer.Dispose() 是危险操作:底层句柄可能被释放,而回调还在执行中,导致 ObjectDisposedException 或静默失败。
- 正确做法是用
Interlocked或volatile bool标记“已取消”,并在回调开头检查 - 配合
timer.Change(Timeout.Infinite, Timeout.Infinite)立即终止后续调度 -
Dispose()放到主线程或安全上下文里调用(比如using块或显式释放)
state 对象共享需线程安全,别传 this 或 UI 控件
state 参数不是每次回调都新建副本,多个回调共享同一对象实例。直接修改它的字段(如 state.Counter++)会引发竞态。
- 简单计数用
Interlocked.Increment(ref counter) - 状态复杂时,优先传不可变对象,或加
lock保护临界区 - 绝对不要把
this、TextBox、Form这类 UI 对象传进state—— 回调线程无法安全访问它们
回调必须自己 try/catch,否则异常会被吞掉
System.Threading.Timer 的回调运行在线程池线程上,未捕获异常不会冒泡,也不会停掉定时器。看起来像“卡住”或“断掉”,其实是错误被静默吞了。
- 每个回调体第一层必须包
try/catch,至少写日志 - 别指望全局异常处理器能捕获它 —— 它不在主线程上下文里
- 如果要更新 UI,必须用
Control.Invoke或Dispatcher.Invoke封送,不能直接赋值Label.Text
最易忽略的其实是精度和生命周期绑定:Windows 默认时钟粒度约 15.6ms,设 period=1 没意义;而 Timer 不会自动随作用域结束,Dispose() 忘了调,线程池资源就一直挂着。


















