System.Threading.Timer是否需等待上一回调完成才触发新回调?
问题解答
1. 关于Timer触发行为的官方说明
System.Threading.Timer的触发逻辑在官方文档的类备注章节有明确记载:
- 计时器设定的周期间隔,是从上一次回调被加入线程池工作队列的时间点开始计算,而非上一次回调执行完成的时间点。
- 只要间隔到期,无论上一次回调是否执行完毕,新的回调都会被排入线程池队列等待执行。
- 示例中使用
async void作为回调会进一步放大这个现象:Timer本身不感知异步等待逻辑,async void方法在执行到第一个await时就会向调用方返回,Timer会直接认为本次回调已经执行结束,完全不会等待await后续的逻辑,因此才会出现每秒都有新回调启动、完全等不到10秒延迟结束的输出。
2. 等待上一次回调完成再触发的实现方式
.NET 没有内置开箱即用的、自动规避重入的System.Threading.Timer配置,你可以根据自己使用的.NET版本选以下两种实现方案,都能完全符合「上一次执行完成后,再等指定间隔触发下一次」的预期:
方案1:手动调度Timer(全版本通用)
初始化Timer时将周期参数设为Timeout.InfiniteTimeSpan,让Timer只触发第一次回调;每次回调的全部逻辑(包括异步等待部分)执行完成后,手动调用Change方法调度下一次触发,从根源上避免重入。
示例代码:
// 初始化时period设为无限超时,仅触发第一次 var timer = new Timer(TimerCallback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); static int i = 0; async Task TimerCallback(object state) { i++; Console.WriteLine($"method {i} started"); try { // 实际业务逻辑 await Task.Delay(10000); Console.WriteLine($"method {i} finished"); } finally { // 无论业务逻辑成功还是抛异常,都调度下一次执行,避免计时器意外停止 timer.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); } }
注意不要使用async void作为Timer回调,否则回调中抛出的未处理异常会直接终止进程,也无法被全局异常捕获。
方案2:使用PeriodicTimer(.NET 6+ 推荐)
.NET 6 新增的PeriodicTimer是专门为异步场景设计的计时器,天然规避重入问题:它没有自动回调机制,需要在代码中主动等待下一个触发信号,执行完业务逻辑后再进入下一轮等待,逻辑更简洁也不容易出错。
示例代码:
var periodicTimer = new PeriodicTimer(TimeSpan.FromSeconds(1)); // 启动计时器循环 _ = RunTimerAsync(periodicTimer); async Task RunTimerAsync(PeriodicTimer pt) { int i = 0; // 等待下一次触发信号,到点才执行逻辑,执行完再回到等待状态 while (await pt.WaitForNextTickAsync()) { i++; Console.WriteLine($"method {i} started"); await Task.Delay(10000); Console.WriteLine($"method {i} finished"); } }
不推荐用锁/信号量包裹回调的方式做重入控制,这种方式会让超过间隔时间到达的回调在线程池队列里排队,既浪费线程池资源,也做不到「执行完再等固定间隔」的效果。
内容的提问来源于stack exchange,提问作者meJustAndrew
相关产品推荐
相关产品推荐

