C#定时器场景下事件处理器并行执行及定时器策略合理性问询
背景
我正在研发一套舞台/演唱会自动化灯光控制系统。简单来说,DMX协议每秒约40次通过线路发送512字节数据,映射到特定字节(通道)的接收灯具会据此调整状态——比如调光器在7通道、值为255时灯具全亮,值为0时熄灭。计算机与DMX接口间通过Art-Net、sACN等网络协议通信,我的代码需要每25ms(每秒40次)向网络发送对应格式的数据包。
发送字节数据的操作难度不高,字节数组打包开销极低,但有大量操作会修改这些数组的值:比如屏幕手动推子输入、灯光淡入淡出或移动算法等。我已经构思了优先级处理逻辑,当前更关注如何让这些操作在刷新周期内完成——每个操作单独执行仅需1-2毫秒,但必须全部在下一次刷新前完成。
当前定时器实现代码
_sendingTimer = new System.Timers.Timer(); _sendingTimer.Interval = _networkInterface.SendingInterval; // 25ms _sendingTimer.AutoReset = false; _sendingTimer.Elapsed += async (_, _) => { SendTheData(); // 平均耗时1-2毫秒 _stateManager.OnUpdateCycle(); _sendingTimer.Start(); }; _sendingTimer.Start();
StateManager相关代码
public void OnUpdateCycle() { UpdateCycled.Invoke(this, EventArgs.Empty); // 这些处理器能否并行执行? } public event EventHandler UpdateCycled;
UpdateCycled事件会绑定大量处理器,主要执行灯光交叉淡入淡出调光值计算这类轻量化数学运算,开启调试或日志时单处理器最多耗时2毫秒。但如果绑定25个处理器,串行执行可能无法在25ms周期内完成。
问题
- 当前定时器设置能否保证以合理精度按间隔执行?若方法执行超时,能否自动放弃上一次任务?理想情况是超时则放弃前一次执行。
- 有没有高效方式让事件处理器并行执行,且充分利用资源确保完成?由于采用UDP协议发送数据包,丢包不影响后续灯具响应,但事件处理器计算未完成会导致多个周期的间隙,使淡入淡出或移动效果不流畅(部分效果遵循正弦曲线,卡顿会很明显)。(假设计算值基于时间而非上一次值)
解答
问题1解答
当前的定时器设置无法保证严格的25ms间隔精度,也不会自动放弃超时的任务:
System.Timers.Timer的精度依赖系统时钟(通常是10-15ms的分辨率),加上回调执行的耗时,实际间隔会有波动。- 你的代码是在回调执行完所有逻辑后才重新启动定时器,属于延迟触发模式:如果
SendTheData()或OnUpdateCycle()的总耗时超过25ms,下一次触发会直接延后,不会跳过超时的任务。
要实现「超时则放弃前一次任务」的逻辑,需要额外的任务跟踪机制:
private Task _currentUpdateTask = Task.CompletedTask; _sendingTimer.Elapsed += async (_, _) => { // 如果上一次任务还在执行,直接跳过 if (!_currentUpdateTask.IsCompleted) { _sendingTimer.Start(); return; } SendTheData(); // 用Task.Run包裹更新逻辑,跟踪任务状态 _currentUpdateTask = Task.Run(() => _stateManager.OnUpdateCycle()); _sendingTimer.Start(); };
这种方式下,如果上一次的OnUpdateCycle()还没执行完,新的周期会直接跳过计算,保证定时器按间隔触发,避免任务堆积。
问题2解答
可以通过并行执行事件处理器来提升效率,同时保证计算在周期内完成:
并行触发事件处理器
默认UpdateCycled.Invoke()是串行执行所有绑定的事件处理器,你可以手动遍历事件委托的调用列表,用Parallel.ForEach并行执行:public void OnUpdateCycle() { var handlers = UpdateCycled; if (handlers == null) return; // 并行执行所有事件处理器 Parallel.ForEach(handlers.GetInvocationList(), handler => { ((EventHandler)handler)(this, EventArgs.Empty); }); }这种方式能利用多核CPU资源,大幅减少总执行时间——25个各耗时2ms的任务,串行需要50ms,并行的话在4核CPU上可能只需要8-10ms(考虑线程调度开销)。
额外优化建议
- 提前计算时间戳:因为你的计算基于时间而非上一次值,在周期开始时就获取当前时间戳,所有处理器共用这个时间戳,避免因执行顺序导致的时间偏差。
- 限制并行度:如果CPU核心数有限,可以通过
ParallelOptions限制最大并行数,避免线程调度开销过大:var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; Parallel.ForEach(handlers.GetInvocationList(), options, handler => { ((EventHandler)handler)(this, EventArgs.Empty); }); - 避免锁竞争:如果事件处理器会修改同一个DMX字节数组,必须保证线程安全——可以为每个通道设置独立的锁,或者使用线程安全的数据结构,避免全局锁导致的并行效率下降。
内容的提问来源于stack exchange,提问作者Jeff Putz

