System.Timers.Timer失步:如何精准触发方法同步视频触觉反馈
解决定时器延迟堆叠问题的优化方案
问题根源
你使用的System.Timers.Timer在AutoReset=true模式下,会在Elapsed事件完全执行完毕后才重新开始计时。如果streamSensation和calcManagerTick的总耗时超过100ms,下一次触发就会被推迟,且延迟会不断累积,这就是运行15秒后滞后2秒的核心原因。
优化方案一:手动重置Timer+时间修正
把Timer改为手动重置模式,结合Stopwatch计算实际需要的等待间隔,抵消延迟堆叠:
private int tick = 0; private System.Timers.Timer timer; private System.Diagnostics.Stopwatch watch; private readonly long targetIntervalMs = 100; // 存储上一次计算的触觉数据,避免计算超时无数据可发 private Sensation _currentSensation; private YourConstructor() { timer = new System.Timers.Timer(targetIntervalMs); timer.Elapsed += OnTimerElapsed; timer.AutoReset = false; // 关闭自动重置,改为手动控制 timer.Enabled = false; // 初始化默认触觉数据 _currentSensation = new Sensation(); } private void OnTimerElapsed(Object source, ElapsedEventArgs e) { // 1. 优先发送上一次计算好的数据 streamSensation(_currentSensation); tick++; // 2. 计算下一次的触觉数据 var newSensation = calcManagerTick(tick); if (newSensation != null) { _currentSensation = newSensation; } // 3. 修正下一次触发间隔,抵消延迟 long expectedMs = tick * targetIntervalMs; long actualMs = watch.ElapsedMilliseconds; // 计算需要等待的时间,若为负则立即触发(用1ms避免0值异常) long nextInterval = Math.Max(1, expectedMs - actualMs); timer.Interval = nextInterval; timer.Start(); } private void streamSensation(Sensation sensation) { // 发送触觉数据逻辑 Console.WriteLine($"{tick} / {watch.ElapsedMilliseconds}"); } private Sensation calcManagerTick(int currentTick) { // 根据当前tick加载并计算触觉数据的逻辑 return new Sensation(); } private void start() { if (!timer.Enabled) { watch = Stopwatch.StartNew(); // 提前计算第一个tick的数据 _currentSensation = calcManagerTick(tick); timer.Start(); } }
方案优势
- 改动量小,符合你原有的Timer使用习惯
- 通过时间修正逻辑,自动抵消单次计算带来的延迟,避免堆叠
- 确保计算超时后,发送的是上一次的有效数据,满足需求3
优化方案二:异步Task循环(更灵活)
用Task.Run结合Task.Delay实现异步定时循环,计算逻辑不阻塞定时流程,彻底避免延迟堆叠:
private int tick = 0; private System.Diagnostics.Stopwatch watch; private readonly long targetIntervalMs = 100; private Sensation _currentSensation; private CancellationTokenSource _cts; private readonly object _dataLock = new object(); private YourConstructor() { _currentSensation = new Sensation(); } private async void start() { if (_cts != null && !_cts.IsCancellationRequested) return; _cts = new CancellationTokenSource(); watch = Stopwatch.StartNew(); tick = 0; while (!_cts.IsCancellationRequested) { // 1. 发送当前存储的触觉数据(优先保证发送时机) lock (_dataLock) { streamSensation(_currentSensation); } tick++; // 2. 异步计算下一次数据,不阻塞定时流程 _ = Task.Run(() => { var newSensation = calcManagerTick(tick); lock (_dataLock) { _currentSensation = newSensation; } }, _cts.Token); // 3. 计算下一次等待时间,修正延迟 long expectedMs = tick * targetIntervalMs; long actualMs = watch.ElapsedMilliseconds; long waitMs = expectedMs - actualMs; // 若计算耗时超过间隔,立即进入下一轮 if (waitMs > 0) { await Task.Delay(TimeSpan.FromMilliseconds(waitMs), _cts.Token); } } } private void streamSensation(Sensation sensation) { // 发送触觉数据逻辑 Console.WriteLine($"{tick} / {watch.ElapsedMilliseconds}"); } private Sensation calcManagerTick(int currentTick) { // 根据当前tick加载并计算触觉数据的逻辑 return new Sensation(); } private void stop() { _cts?.Cancel(); _cts?.Dispose(); _cts = null; }
方案优势
- 计算逻辑异步执行,完全不影响定时发送的节奏
- 天然支持计算超时场景,自动重复发送上一次数据
- 后续若要和视频播放时间同步,可直接在循环中获取视频当前播放时间修正
tick,实现更精准的同步
额外同步建议
如果要和视频定时器更精准同步,不要完全依赖本地Stopwatch,可以定期获取视频的当前播放时间(比如每2-3秒一次),然后修正本地tick值:
// 假设视频播放控件提供GetCurrentPlaybackTimeMs()方法 long videoCurrentMs = videoControl.GetCurrentPlaybackTimeMs(); tick = (int)(videoCurrentMs / targetIntervalMs);
这样可以抵消本地时间和视频时间的漂移,确保触觉触发和视频内容对齐。
内容的提问来源于stack exchange,提问作者Kingfinity
相关产品推荐
相关产品推荐

