1

My C# application has to execute a task every few seconds. It is very important that the execution happens at exactly this interval; give or take a few milliseconds.

I tried using a Timer but the time gradually shifts after a few minutes. The code used by me is as follows:

System.Timers.Timer timerObj = new System.Timers.Timer(10 * 1000);
timerObj.Elapsed += timerObj_Elapsed;
timerObj.AutoReset = true;
timerObj.Start();

static void timerObj_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
  DateTime currentTime = DateTime.Now;
  Console.WriteLine(currentTime.ToString("HH:mm:ss.fff"));
}

enter image description here

Is there a better way to do this kind of activity?

Anil Mathew
  • 2,590
  • 1
  • 15
  • 15

3 Answers3

3

If it's really important to be that precise, set your timer's interval to something smaller than the max number of milliseconds by which you can be off. (Hopefully this will be greater than 15ms, as that's the resolution of System.Timers.Timer.) Then, in the tick handler, check whether the appropriate amount of time has passed and, if so, call the "real" handler. If your goal is to avoid drift, your test of whether it's time to fire should be based on the time elapsed since starting the timer, not the time elapsed since the last "tick."

adv12
  • 8,443
  • 2
  • 24
  • 48
  • Could you please explain your last point: "If your goal is to avoid drift, your test of whether it's time to fire should be based on the time elapsed since starting the timer, not the time elapsed since the last "tick." – Anil Mathew Sep 16 '16 at 19:46
  • Same with the backgroundworker class - you don't test based on ticks - test based on actual time spent since the timer was started. test in a loop, decrease your sleep time in between tests as your time approaches such that at the end, it's smpling multiple times per millisecond. Fire your event, and begin the process over, increasing your sleep time to 1000 (assuming your repeat is on the order of multiple seconds); – Shannon Holsinger Sep 16 '16 at 19:56
  • +1 The most important point in the answer is Windows can't do time sensitive stuff down under 15ms. haven't the exact minimum period (machine/motherboard is) will seem to skip/drift when measured at millisecond level. – kenny Sep 16 '16 at 20:16
  • good info and links here http://stackoverflow.com/questions/29999274/timer-firing-tick-event-with-15-milliseconds-delay – kenny Sep 16 '16 at 20:17
  • @AnilMathew to phrase it another way, you record the time when you started the timer in a variable. You set your timer tick to something shorter than your tolerance, say, 100ms. When your timer event fires you check the difference between the current time and your start time to see if you have passed the next 5 second interval. This works well enough but there are many trade offs (efficiency, accuracy). Ultimately I agree with others (and with my experience) that windows and accurate timing don't play well together. – plast1k Sep 16 '16 at 20:36
1

You could try to always schedule a singelshot timer via AutoReset = false and calculate the delta on which the timer should fire. This should compensate your skew as it calculates the delta from the absolute time. Here a rough example:

// member variables
DateTime firstSchedule = DateTime.UtcNow;
var numElapsed = 1;

constructor()
{
    this.timerObj = new System.Timers.Timer();
    timerObj.Interval = CalcDelta();
    timerObj.Elapsed += timerObj_Elapsed;
    timerObj.AutoReset = false;
    timerObj.Start();
}

void timerObj_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    this.numElapses++;
    this.timerObj.Interval = CalcDelta();
    this.timerObj.Start();

    DateTime currentTime = DateTime.Now;
    Console.WriteLine(currentTime.ToString("HH:mm:ss.fff"));
}


private long CalcDelta()
{
    DateTime nextSchedule = firstSchedule + TimeSpan.FromSeconds(numElapses * 10);
    return (nextSchedule - DateTime.UtcNow).TotalMilliseconds;  
}
Nico
  • 3,542
  • 24
  • 29
  • I've tried an approach like this that definitely mitigated the problem, but over the course of a few hours it always eventually drifted enough to show in my data (1 second timestamps). – plast1k Sep 16 '16 at 20:38
0
    private void setTimerRepeat(object sender, DoWorkEventArgs e){
        DateTime begin = DateTime.Now;
        bool isRunning = true;
        int sleep=500;
        while(isRunning){
            int milliSeconds = DateTime.Now.Subtract(begin).TotalMilliSeconds;
            if(milliSeconds > 9000){
                sleep=10;
            }else{
                sleep=500;
            }
            if(milliSeconds=>10000){//if you get drift here, it should be consistent - adjust firing time downward to offset drift (change sleep to a multiple such that sleep%yourNumber==0)
                begin = DateTime.Now;
                Task.Run(()=>fireEvent());
            }
            Thread.Sleep(sleep);

        }
   }
}
Shannon Holsinger
  • 2,293
  • 1
  • 15
  • 21