C#微定时器任务超期并行执行疑问及规避方案咨询
问题与解决方案
问题描述
我在C#脚本中使用一款微定时器DLL,要求某函数在20秒内严格按每50ms执行一次。但受网络影响,该函数有时执行耗时会超过50ms。
我给微定时器的回调函数加了计数器,20秒后如果计数器值为400,就认为所有执行都按时完成;但有时计数器值会大于400,我推测是前一次函数还没执行完,定时器就再次触发,导致函数并行执行,计数器被重复累加了。
现在有两个问题:
- 关于计数器值大于400的原因,我的推测是否正确?
- 当函数执行耗时超过50ms时,怎么避免前一次还没执行完就触发下一次?如果耗时小于60ms可以容忍,需要在本次执行完成后再启动下一次,同时尽量保持每50ms执行一次的规则。
附当前代码:
public async void OnTimedEvent_MicroTimer(object sender, MicroLibrary.MicroTimerEventArgs timerEventArgs) { object microcount; if (Measuring_period_finished == false) // 结束信号来自另一个定时器 { Counter += 1; Periodic_funciton(); } if (Measuring_period_finished == true) { if( Counter > 400) { delayed_functions=Counter-400; message="function executed "+delayed_functions.toString()+" times delayed"; } if( Counter == 400); { message="function executed always in time"; } } }
解答
1. 计数器值大于400的原因推测是否正确?
是的,你的推测完全正确。这款微定时器的默认逻辑是不管回调函数是否执行完毕,都会按设定的固定间隔触发下一次回调,属于典型的“基于间隔”触发机制。当Periodic_funciton()执行耗时超过50ms时,下一次定时器触发会直接开启新线程执行回调,导致多次回调并行运行,每次都会执行Counter += 1,最终计数器自然会超过400。
2. 如何避免前一次未完成就触发下一次?
要实现“本次执行完成后再安排下一次”的逻辑,同时尽量贴近50ms的执行间隔,可以按以下方案修改:
核心思路
- 加一个线程安全的执行锁,标记当前是否正在运行目标函数,避免并行执行;
- 记录每次执行的结束时间,根据实际耗时调整下一次触发的间隔,尽量补回时间偏差(满足耗时小于60ms的容忍条件)。
修改后的基础版本代码
// 新增线程安全的执行锁,避免并行执行 private readonly object _executionLock = new object(); public void OnTimedEvent_MicroTimer(object sender, MicroLibrary.MicroTimerEventArgs timerEventArgs) { // 尝试获取锁,获取不到说明当前正在执行,直接跳过本次触发 if (!Monitor.TryEnter(_executionLock)) { return; } try { if (!Measuring_period_finished) { Counter += 1; Periodic_funciton(); } if (Measuring_period_finished) { if (Counter > 400) { delayed_functions = Counter - 400; message = $"函数延迟执行了 {delayed_functions} 次"; } else if (Counter == 400) { message = "所有函数执行均按时完成"; } } } finally { // 确保锁被释放,避免死锁 Monitor.Exit(_executionLock); } }
进阶优化(贴近50ms间隔)
如果想在任务完成后尽量补回时间偏差,让整体执行节奏更接近50ms间隔,可以在每次执行完成后计算调整下一次的定时器间隔:
// 定时器实例 private MicroLibrary.MicroTimer _microTimer; // 记录上一次执行的结束时间 private DateTime _lastExecutionEndTime; // 初始化定时器的方法 public void InitMicroTimer() { _microTimer = new MicroLibrary.MicroTimer(); _microTimer.Interval = 50; // 初始间隔设为50ms _microTimer.MicroTimerElapsed += OnTimedEvent_MicroTimer; _lastExecutionEndTime = DateTime.Now; } public void OnTimedEvent_MicroTimer(object sender, MicroLibrary.MicroTimerEventArgs timerEventArgs) { if (!Monitor.TryEnter(_executionLock)) { return; } try { if (!Measuring_period_finished) { Counter += 1; Periodic_funciton(); DateTime currentEndTime = DateTime.Now; // 计算本次执行耗时 TimeSpan executionCost = currentEndTime - _lastExecutionEndTime; // 调整下一次间隔:如果耗时小于60ms,补回偏差;否则按50ms重新开始 int nextInterval = executionCost.TotalMilliseconds < 60 ? Math.Max(1, 50 - (int)executionCost.TotalMilliseconds) : 50; _microTimer.Interval = nextInterval; _lastExecutionEndTime = currentEndTime; } // 后续统计逻辑同基础版本 if (Measuring_period_finished) { if (Counter > 400) { delayed_functions = Counter - 400; message = $"函数延迟执行了 {delayed_functions} 次"; } else if (Counter == 400) { message = "所有函数执行均按时完成"; } } } finally { Monitor.Exit(_executionLock); } }
注意事项
- 原代码中
async void的回调可能引发线程上下文混乱,建议改为同步逻辑,或者确认定时器支持async Task类型的回调; - 如果需要允许任务排队(而非跳过),可以用
SemaphoreSlim替代Monitor,但要注意避免任务堆积导致内存溢出; - 代码中
Math.Max(1, ...)是为了避免间隔设为0导致定时器异常。
内容的提问来源于stack exchange,提问作者Mustafa Levrek
相关产品推荐
相关产品推荐

