Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

System.Threading.Timer call drifts a couple seconds every day

I have a service that is always running, it has a timer to perform a particular action every day at 2AM.

TimeSpan runTime = new TimeSpan(2, 0, 0); // 2 AM
TimeSpan timeToFirstRun = runTime - DateTime.Now.TimeOfDay;

if (timeToFirstRun.TotalHours < 0)
{
    timeToFirstRun += TimeSpan.FromDays(1.0);
}

_dailyNodalRunTimer = new Timer(
    RunNodalDailyBatch,
    null,
    timeToFirstRun,
    TimeSpan.FromDays(1.0)); //repeat event daily

That initialization code is called once when the service first starts, over the past few days I have logged when the Timer has fired:

2011-05-21 02:00:01.580
2011-05-22 02:00:03.840
...
2011-05-31 02:00:25.227
2011-06-01 02:00:27.423
2011-06-02 02:00:29.847

As you can see its drifting by 2 seconds every day, getting farther and farther from when it was supposed to fire(at 2 AM).

Am I using it wrong or is this Timer not designed to be accurate? I could recreate the timer each day, or have it fire at some small interval and repeatedly check if I want to perform the action, but that seems kind of hacky.

EDIT

I tried using System.Timers.Timer and it appears to have the same issue. The reseting the Interval is because you cant schedule the initial time before the first tick in System.Timers.Timer like you can in System.Threading.Timer

int secondsInterval = 5;

double secondsUntilRunFirstRun = secondsInterval - (DateTime.Now.TimeOfDay.TotalSeconds % secondsInterval);
var timer = new System.Timers.Timer(secondsUntilRunFirstRun * 1000.0);
timer.AutoReset = true;
timer.Elapsed += (sender, e) =>
    {
        Console.WriteLine(DateTime.Now.ToString("hh:mm:ss.fff"));

        if (timer.Interval != (secondsInterval * 1000.0))
            timer.Interval = secondsInterval * 1000.0;
    };
timer.Start();

Produce the following times, you can see how they are drifting slightly:

06:47:40.020
06:47:45.035
06:47:50.051
...
06:49:40.215
06:49:45.223
06:49:50.232

So I guess the best approach really is to just reschedule the timer in the tick handler? The following produces a tick at a regular interval within ~15 milliseconds

double secondsUntilRunFirstRun = secondsInterval - (DateTime.Now.TimeOfDay.TotalSeconds % secondsInterval);

var timer = new System.Timers.Timer(secondsUntilRunFirstRun * 1000.0);
timer.AutoReset = false;
timer.Elapsed += (sender, e) =>
{
    Console.WriteLine(DateTime.Now.ToString("hh:mm:ss.fff"));

    timer.Interval = (secondsInterval - (DateTime.Now.TimeOfDay.TotalSeconds % secondsInterval)) * 1000.0;
};
timer.Start();


06:51:45.009
06:51:50.001
...
06:52:50.011
06:52:55.013
06:53:00.001
like image 669
BrandonAGr Avatar asked Dec 12 '22 12:12

BrandonAGr


1 Answers

Don't let timer inaccuracies accumulate. Use the RTC to calculate how many ms remain until the timeout time. Sleep/setInterval to half this time. When the timer fires/sleep returns, use the RTC again to recalculate the interval left and set interval/sleep again to half-life. Repeat this loop until the remaining interval is less than 50ms. Then CPU loop on the RTC until the desired time is exceeded. Fire the event.

Rgds, Martin

like image 126
Martin James Avatar answered Mar 03 '23 10:03

Martin James