system.threading.timer 创建即启动且回调在threadpool线程执行;period=0表示单次执行而非禁用;dispose()不可在回调中直接调用,须用change()配合状态标记;state对象共享需线程安全处理;定时精度受系统限制,短周期不可靠。

System.Threading.Timer 不是“启动后自动循环”的傻瓜式定时器,它一创建就按构造参数开始计时,且回调在 ThreadPool 线程执行——这点不理解,后续必踩资源泄漏、线程竞争、回调丢失的坑。
构造时 period=0 表示只执行一次,不是“禁用”
很多人误以为 period=0 是关闭周期执行,实际含义是:首次回调后不再重复触发。它等价于“单次延迟执行”。
-
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() 是危险操作:Dispose() 会释放底层句柄,但回调可能仍在执行中,导致 ObjectDisposedException 或静默失败。
- 正确做法是用一个
bool字段或Interlocked标记“已取消”,并在回调开头检查 - 再配合
timer.Change(Timeout.Infinite, Timeout.Infinite)停止后续触发 - 最后在安全时机(如主线程)调用
Dispose()
示例关键逻辑:
private static bool _shouldStop = false;
private static void TimerCallback(object state)
{
if (_shouldStop) return;
Console.WriteLine("执行中...");
// 满足条件后准备停
if (someCondition) {
_shouldStop = true;
timer.Change(Timeout.Infinite, Timeout.Infinite); // 立即停后续
}
}
state 参数别传引用类型并直接修改,小心线程竞争
state 对象会在每次回调中传入,但它不是副本——多个回调共享同一对象实例。如果回调里直接修改它的字段(比如 state.Counter++),会引发竞态。
- 简单计数请用
Interlocked.Increment(ref counter) - 状态复杂时,优先用不可变对象,或加锁(
lock)保护临界区 - 避免把
this或 UI 控件传进state,回调线程无法安全访问它们
定时不准?检查 dueTime/period 单位和系统负载
dueTime 和 period 是毫秒,但实际精度受系统调度影响。Windows 默认时钟粒度约 15.6ms,高频短周期(如 period=1)根本不可靠。
- 低于 10ms 的周期基本无效,建议最低设为 50ms 起
- 高负载下回调可能堆积(ThreadPool 队列满),表现为“突然连发多次”
- 若需严格准时,改用
System.Timers.Timer(带AutoReset和事件模型)或PeriodicTimer(.NET 6+)
真正难处理的是:回调执行时间超过 period 本身。这时下一次回调会排队等待,不会跳过——这不是 bug,是设计如此。











