经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
.NET中如何实现高精度定时器
来源:cnblogs  作者:czwy  时间:2023/12/21 9:05:05  对本文有异议

.NET中有多少种定时器一文介绍过.NET中至少有6种定时器,但精度都不是特别高,一般在15ms~55ms之间。在一些特殊场景,可能需要高精度的定时器,这就需要我们自己实现了。本文将讨论高精度定时器实现的思路。

高精度定时器

一个定时器至少需要考虑三部分功能:计时、等待、触发模式。计时是进行时间检查,调整等待的时间;等待则是用来跳过指定的时间间隔。触发模式是指定时器每次Tick的时间固定还是每次定时任务时间间隔固定。比如定时器时间间隔10ms,定时任务耗时7ms,是每隔10ms触发一次定时任务,还是等定时任务执行完后等10ms再触发下一个定时任务。

计时

Windows提供了可用于获取高精度时间戳或者测量时间间隔的API。系统原生API是QueryPerformanceCounter (QPC)。在.NET中提供了System.Diagnostics.Stopwatch类获取高精度时间戳,它内部也是通过QueryPerformanceCounter (QPC)进行高精度计时。
QueryPerformanceCounter (QPC)使用硬件计数器作为其基础。硬件计时器由三个部分组成:时钟周期生成器、计数时钟周期的计数器和检索计数器值的方法。这三个分量的特征决定了QueryPerformanceCounter (QPC)的分辨率、精度、准确性和稳定性[1]。它的精度可以高达几十纳秒,用来实现高精度定时器基本没什么问题。

等待

等待策略通常有两种:

  • 自旋:让CPU空转等待,一直占用CPU时间。
  • 阻塞:让线程进入阻塞状态,出让CPU时间片,满足等待时间后切换回运行状态。

自旋等待

自旋等待可以使用Thread.SpinWait(int iteration)来实现,参数iteration是迭代次数。由于CPU速度可能是动态的,所以很难根据iteration计算消耗的时间,最好是结合Stopwatch使用:

  1. void Spin(Stopwatch w, int duration)
  2. {
  3. var current = w.ElapsedMilliseconds;
  4. while ((w.ElapsedMilliseconds - current) < duration)
  5. Thread.SpinWait(5);
  6. }

由于自旋是以消耗CPU为代价的,上述代码运行时,CPU处于满负荷工作状态(使用率持续保持100%左右),因此短暂的等待可以考虑自旋,长时间运行的定时器不太建议使用该方法。

阻塞等待

阻塞等待需要操作系统能够及时把定时器线程调度回运行状态。默认情况下,Windows的系统的计时器精度为15ms左右。如果是线程阻塞,出让其时间片进行等待,然后再被调度运行的时间至少是一个时间切片15ms左右。要通过阻塞实现高精度计时,则需要减少时间切片的长度。Windows系统API提供了timeEndPeriod可以把计时器精度修改到1ms,在使用计时器服务之前立即调用timeEndPeriod,并在使用完计时器服务后立即调用timeEndPeriodtimeEndPeriodtimeEndPeriod必须成对出现。

在Windows 10, version 2004之前,timeEndPeriod会影响全局Windows设置,所有进程都会使用修改后的计时精度。从Windows 10, version 2004开始,只有调用timeEndPeriod的进程受到影响。
设置更高的精度可以提高等待函数中超时间隔的准确性。 但是,它也可能会降低整体系统性能,因为线程计划程序更频繁地切换任务。 高精度还可以阻止 CPU 电源管理系统进入节能模式。 设置更高的分辨率不会提高高分辨率性能计数器的准确性。[2]

通常我们使用Thread.Sleep来挂起线程等待,Sleep的参数最小为1ms,但实际上很不稳定,实测发现大部分时候稳定在阻塞2ms。我们可以采用Sleep(0)或者Thread.Yield结合Stopwatch计时的方式修正。

  1. void wait(Stopwatch w, int duration)
  2. {
  3. var current = w.ElapsedMilliseconds;
  4. while ((w.ElapsedMilliseconds - current) < duration)
  5. Thread.Sleep(0);
  6. }

Thread.Sleep(0)和Thread.Yield在 CPU 高负载情况下非常不稳定,可能会产生更多的误差。因此误差修正最好通过自旋方式实现。

还有一种阻塞的方式是多媒体定时器timeSetEvent,也是网上关于高精度定时器提得比较多的一种方式。它是winmm.dll中的函数,稳定性和精度都比较高,能提供1ms的精度。
官方文档中说timeSetEvent是一个过时的方法,建议使用CreateTimerQueueTimer替代[3]。但CreateTimerQueueTimer的精度和稳定性都不如多媒体定时器,所以在需要高精度定时器时,还是要用timeSetEvent。以下是封装多媒体定时器的例子

  1. public enum TimerError
  2. {
  3. MMSYSERR_NOERROR = 0,
  4. MMSYSERR_ERROR = 1,
  5. MMSYSERR_INVALPARAM = 11,
  6. MMSYSERR_NOCANDO = 97,
  7. }
  8. public enum RepeateType
  9. {
  10. TIME_ONESHOT=0x0000,
  11. TIME_PERIODIC = 0x0001
  12. }
  13. public enum CallbackType
  14. {
  15. TIME_CALLBACK_FUNCTION = 0x0000,
  16. TIME_CALLBACK_EVENT_SET = 0x0010,
  17. TIME_CALLBACK_EVENT_PULSE = 0x0020,
  18. TIME_KILL_SYNCHRONOUS = 0x0100
  19. }
  20. public class HighPrecisionTimer
  21. {
  22. private delegate void TimerCallback(int id, int msg, int user, int param1, int param2);
  23. [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps")]
  24. private static extern TimerError TimeGetDevCaps(ref TimerCaps ptc, int cbtc);
  25. [DllImport("winmm.dll", EntryPoint = "timeSetEvent")]
  26. private static extern int TimeSetEvent(int delay, int resolution, TimerCallback callback, int user, int eventType);
  27. [DllImport("winmm.dll", EntryPoint = "timeKillEvent")]
  28. private static extern TimerError TimeKillEvent(int id);
  29. private static TimerCaps _caps;
  30. private int _interval;
  31. private int _resolution;
  32. private TimerCallback _callback;
  33. private int _id;
  34. static HighPrecisionTimer()
  35. {
  36. TimeGetDevCaps(ref _caps, Marshal.SizeOf(_caps));
  37. }
  38. public HighPrecisionTimer()
  39. {
  40. Running = false;
  41. _interval = _caps.periodMin;
  42. _resolution = _caps.periodMin;
  43. _callback = new TimerCallback(TimerEventCallback);
  44. }
  45. ~HighPrecisionTimer()
  46. {
  47. TimeKillEvent(_id);
  48. }
  49. public int Interval
  50. {
  51. get { return _interval; }
  52. set
  53. {
  54. if (value < _caps.periodMin || value > _caps.periodMax)
  55. throw new Exception("invalid Interval");
  56. _interval = value;
  57. }
  58. }
  59. public bool Running { get; private set; }
  60. public event Action Ticked;
  61. public void Start()
  62. {
  63. if (!Running)
  64. {
  65. _id = TimeSetEvent(_interval, _resolution, _callback, 0,
  66. (int)RepeateType.TIME_PERIODIC | (int)CallbackType.TIME_KILL_SYNCHRONOUS);
  67. if (_id == 0) throw new Exception("failed to start Timer");
  68. Running = true;
  69. }
  70. }
  71. public void Stop()
  72. {
  73. if (Running)
  74. {
  75. TimeKillEvent(_id);
  76. Running = false;
  77. }
  78. }
  79. private void TimerEventCallback(int id, int msg, int user, int param1, int param2)
  80. {
  81. Ticked?.Invoke();
  82. }
  83. }

触发模式

由于定时任务执行时间不确定,并且可能耗时超过定时时间间隔,定时器的触发可能会有三种模式:固定时间框架,可推迟时间框架,固定等待时间。

  • 固定时间框架:尽量按照设定的时间来执行任务,只要任务不是始终超时,就可以回到原来的时间框架上
  • 可推迟时间框架:也是尽量按照设定的时间执行任务,但是超时的任务会推迟时间框架。
  • 固定等待时间:不管任务执行时长,每次任务执行结束到下一次任务开始执行间的等待时间固定。

假定时间间隔为10ms,任务执行的时间在7~11ms之间,下图中显示了三种触发模式的区别。
image

其实还有一种触发模式:任务执行时长大于时间间隔时,只要时间间隔一到,就执行定时任务,多个定时任务并发执行。之所以这里没有提及这种模式,是因为在高精度定时场景中,执行任务的时间开销很有可能大于定时器的时间间隔,如果开启新线程执行定时任务,可能会占用大量线程,这个需要结合实际情况考虑如何执行定时任务。这里讨论的是默认在定时器线程上执行定时任务。


  1. https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#low-level-hardware-clock-characteristics ??

  2. https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod?redirectedfrom=MSDN ??

  3. https://learn.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)?redirectedfrom=MSDN ??

原文链接:https://www.cnblogs.com/czwy/p/17915333.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号