1024programmer Asp.Net How to implement high-precision timer in .NET

How to implement high-precision timer in .NET

How to implement high-precision timer in .NET

The article How many kinds of timers are there in .NET introduced at least 6 kinds of timers in .NET, but the accuracy is not particularly high, generally between 15ms~55ms. In some special scenarios, a high-precision timer may be required, which requires us to implement it ourselves. This article will discuss ideas for implementing high-precision timers.

High-precision timer

A timer needs to consider at least three functions: timing, waiting, and trigger mode. Timing is used to check the time and adjust the waiting time; waiting is used to skip the specified time interval. The trigger mode specifies whether the time of each tick of the timer is fixed or the time interval of each scheduled task is fixed. For example, if the timer interval is 10ms and the scheduled task takes 7ms, should the scheduled task be triggered every 10ms, or should the scheduled task be executed and wait 10ms before triggering the next scheduled task.

timing

Windows provides APIs that can be used to obtain high-precision timestamps or measure time intervals. The system’s native API is QueryPerformanceCounter (QPC). The System.Diagnostics.Stopwatch class is provided in .NET to obtain high-precision timestamps. It also uses QueryPerformanceCounter (QPC) internally for high-precision timing.
QueryPerformanceCounter (QPC) uses a hardware counter as its basis. A hardware timer consists of three parts: a clock cycle generator, a counter that counts clock cycles, and a method to retrieve the counter value. The characteristics of these three components determine the resolution, precision, accuracy and stability of QueryPerformanceCounter (QPC)[1]. Its accuracy can be as high as tens of nanoseconds, and there is basically no problem in implementing a high-precision timer.

wait

There are usually two waiting strategies:

  • Spin: Let the CPU idle and wait, taking up CPU time.
  • Blocking: Let the thread enter the blocking state, transfer the CPU time slice, and switch back to the running state after the waiting time is met.

Spin Wait

Spin waiting can be implemented using Thread.SpinWait(int iteration). The parameter iteration is the number of iterations. Since the CPU speed may be dynamic, it is difficult to calculate the time consumed based on iteration. It is best to use it in conjunction with Stopwatch:

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

Since spin comes at the expense of CPU consumption, when the above code is running, the CPU is at full load (the usage rate continues to remain around 100%), so spin can be considered for short waits and long-running timers. This method is not recommended.

Blocking Wait

Blocking waiting requires the operating system to schedule the timer thread back to the running state in time. By default, the timer accuracy of Windows systems is about 15ms. If the thread is blocked, give up its time slice to wait, and then the time it takes to be scheduled to run is at least a time slice of about 15ms. To achieve high-precision timing with blocking, the length of the time slice needs to be reduced. The Windows system API provides timeBeginPeriod to modify the timer accuracy to 1ms. Call timeBeginPeriod immediately before using the timer service, and call immediately after using the timer service. code>timeEndPeriod. timeBeginPeriod and timeEndPeriod must appear in pairs.

Prior to Windows 10, version 2004, timeBeginPeriod would affect global Windows settings, and all processes would use the modified timing precision. Starting with Windows 10, version 2004, only processes calling timeBeginPeriod are affected.
Setting a higher precision improves the accuracy of the timeout interval in the wait function. However, it may also reduce overall system performance because the thread scheduler switches tasks more frequently. High accuracy also prevents the CPU power management system from entering power-saving modes. Setting a higher resolution does not improve the accuracy of high-resolution performance counters. [2]

Usually we use Thread.Sleep to suspend thread waiting. The minimum parameter of Sleep is 1ms, but it is actually very unstable. According to actual measurements, it is found that it is stable at blocking for 2ms most of the time. We can use Sleep(0) or Thread.Yield combined with Stopwatch timing to correct it.

void wait(Stopwatch w, int duration)
 {
     var current = w.ElapsedMilliseconds;
     while ((w.ElapsedMilliseconds - current) < duration)
         Thread.Sleep(0);
 }
 

Thread.Sleep(0) and Thread.Yield are very unstable under high CPU load and may produce more errors. Therefore error correction is best achieved by spin.

Another blocking method is the multimedia timer timeSetEvent, which is also a method that has been mentioned frequently on the Internet about high-precision timers. It is a function in winmm.dll. It has high stability and accuracy and can provide an accuracy of 1ms.
The official documentation says that timeSetEvent is an obsolete method, and it is recommended to use CreateTimerQueueTimer instead of [3]. However, the accuracy and stability of CreateTimerQueueTimer are not as good as those of multimedia timers, so when a high-precision timer is needed, timeSetEvent should still be used. The following is an example of encapsulating a multimedia timer

public enum TimerError
 {
     MMSYSERR_NOERROR = 0,
     MMSYSERR_ERROR = 1,
     MMSYSERR_INVALPARAM = 11,
     MMSYSERR_NOCANDO = 97,
 }

 public enum RepeatType
 {
     TIME_ONESHOT=0x0000,
     TIME_PERIODIC = 0x0001
 }

 public enum CallbackType
 {
     TIME_CALLBACK_FUNCTION = 0x0000,
     TIME_CALLBACK_EVENT_SET = 0x0010,
     TIME_CALLBACK_EVENT_PULSE = 0x0020,
     TIME_KILL_SYNCHRONOUS = 0x0100
 }

 public class HighPrecisionTimer
 {
     private delegate void TimerCallback(int id, int msg, int user, int param1, int param2);

     [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps")]
     private static extern TimerError TimeGetDevCaps(ref TimerCaps ptc, int cbtc);

     [DllImport("winmm.dll", EntryPoint = "timeSetEvent")]
     private static extern int TimeSetEvent(int delay, int resolution, TimerCallback callback, int user, int eventType);

     [DllImport("winmm.dll", EntryPoint = "timeKillEvent")]
     private static extern TimerError TimeKillEvent(int id);

     private static TimerCaps _caps;
     private int _interval;
     private int _resolution;
     private TimerCallback _callback;
     private int _id;

     staticHighPrecisionTimer()
     {
         TimeGetDevCaps(ref _caps, Marshal.SizeOf(_caps));
     }

     publicHighPrecisionTimer()
     {
         Running = false;
         _interval = _caps.periodMin;
         _resolution = _caps.periodMin;
         _callback = new TimerCallback(TimerEventCallback);
     }

     ~HighPrecisionTimer()
     {
         TimeKillEvent(_id);
     }

     public int Interval
     {
         get { return _interval; }
         set
         {
             if (value  _caps.periodMax)
                 throw new Exception("invalid Interval");
             _interval = value;
         }
     }

     public bool Running { get; private set; }

     public event Action Ticked;

     public void Start()
     {
         if (!Running)
         {
             _id = TimeSetEvent(_interval, _resolution, _callback, 0,
                 (int)RepeateType.TIME_PERIODIC | (int)CallbackType.TIME_KILL_SYNCHRONOUS);
             if (_id == 0) throw new Exception("failed to start Timer");
             Running = true;
         }
     }

     public void Stop()
     {
         if (Running)
         {
             TimeKillEvent(_id);
             Running = false;
         }
     }

     private void TimerEventCallback(int id, int msg, int user, int param1, int param2)
     {
         Ticked?.Invoke();
     }
 }
 

Trigger Mode

Since the execution time of scheduled tasks is uncertain and may take longer than the scheduled time interval, the timer may be triggered in three modes: fixed time frame, deferrable time frame, and fixed waiting time.

  • Fixed time frame: Try to execute the task according to the set time. As long as the task does not always time out, you can return to the original time frame
  • The time frame can be postponed: Try to execute the task according to the set time, but the timeout task will postpone the time frame.
  • Fixed waiting time: Regardless of the task execution time, the waiting time between the end of each task execution and the start of the next task is fixed.

Assuming that the time interval is 10ms and the task execution time is between 7 and 11ms, the following figure shows the difference between the three trigger modes.
image

In fact, there is also a trigger mode: when the task execution time is longer than the time interval, as soon as the time interval comes, the scheduled task will be executed, and multiple scheduled tasks will be executed concurrently. The reason why this mode is not mentioned here is that in high-precision timing scenarios, the time cost of executing tasks is likely to be greater than the time interval of the timer. If a new thread is started to execute a scheduled task, a large number of threads may be occupied. This requires Consider how to perform scheduled tasks based on the actual situation. What is discussed here is that scheduled tasks are executed on the timer thread by default.


  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 ↩︎

This article is from the internet and does not represent1024programmerPosition, please indicate the source when reprinting:https://www.1024programmer.com/how-to-implement-high-precision-timer-in-net/

author: admin

Previous article
Next article

Leave a Reply

Your email address will not be published. Required fields are marked *

Contact Us

Contact us

181-3619-1160

Online consultation: QQ交谈

E-mail: [email protected]

Working hours: Monday to Friday, 9:00-17:30, holidays off

Follow wechat
Scan wechat and follow us

Scan wechat and follow us

Follow Weibo
Back to top
首页
微信
电话
搜索