-1

I'm trying to create a method in C# whereby I can repeatedly perform an action (in my particular application it's sending a UDP packet) at a targeted rate. I know that timer inaccuracy in C# will prevent the output from being precisely at the target rate, but best effort is good enough. However, at higher rates, the timing seems to be completely off.

while (!token.IsCancellationRequested)
{
    stopwatch.Restart();

    Thread.Sleep(5); // Simulate process

    int processTime = (int)stopwatch.ElapsedMilliseconds;
    int waitTime = taskInterval - processTime;

    Task.Delay(Math.Max(0, waitTime)).Wait();
}

See here for the full example console app.

When run, the FPS output of this test app shows around 44-46 Hz for a target of 60 Hz. However at lower rates (say 20 Hz), the output rate is much closer to the target. I can't understand why this would be the case. What is the problem with my code?

pixsperdavid
  • 147
  • 3
  • 14
  • Do you get negative `waitTime` values? If this is the case, then you are not able to process enough requests per seconds. – Yacoub Massad Apr 24 '16 at 20:46
  • 1
    By the way, please post the code example in the question it self. Try to come with a minimal example. – Yacoub Massad Apr 24 '16 at 20:47
  • No, when running at a target rate of 60 Hz my waitTime values are 10-11 ms. And this is the most minimal example that can demonstrate the problem. – pixsperdavid Apr 24 '16 at 20:49
  • 1
    Take a look at this: http://stackoverflow.com/questions/31742521/accuracy-of-task-delay – Yacoub Massad Apr 24 '16 at 20:51
  • The timer resolution is also in the 50-60 Hz range. Do include a minimal sample in the question itself, right now this is eligible for closing. – H H Apr 24 '16 at 20:51
  • One way to fix this might be a start the timer once, and then have a loop where you delay some ~5 ms in each iteration. Inside each iteration, you calculate how much times the operation should have been executed so far and you compare it with how many times you have run it so far. And you then run the operation enough times to catch up. This can be done both synchronously and asynchronously. – Yacoub Massad Apr 24 '16 at 20:53
  • After looking at @YacoubMassad 's link, I can use the ManualResetEvent method rather than Thread.Sleep, which has the same effect. – pixsperdavid Apr 24 '16 at 21:07
  • It is well known that the Windows timing accuracy is low. A Google search will turn up a ton of results. @YacoubMassad, you should post that as an answer. – usr Apr 24 '16 at 21:12
  • @usr As I stated in the question "I know that timer inaccuracy in C# will prevent the output from being precisely at the target rate". The issue however is that rather than an expected ~16ms inaccuracy the rate of the loop is considerably off target. – pixsperdavid Apr 24 '16 at 21:45
  • OK, I now see that so far nobody has fully explained the root cause: The actual delay is always rounded *up*, never down. That causes skew. Maybe that's the key insight for you? The skew is present at all delays/frequencies, even at 20Hz. It's less percentage-wise. – usr Apr 24 '16 at 21:57

2 Answers2

2

The problem is that Thread.Sleep (or Task.Delay) is not very accurate. Take a look at this: Accuracy of Task.Delay

One way to fix this is to a start the timer once, and then have a loop where you delay some ~15 ms in each iteration. Inside each iteration, you calculate how much times the operation should have been executed so far and you compare it with how many times you have run it so far. And you then run the operation enough times to catch up.

Here is some code sample:

private static void timerTask(CancellationToken token)
{
    const int taskRateHz = 60;

    var stopwatch = new Stopwatch();

    stopwatch.Start();

    int ran_so_far = 0;

    while (!token.IsCancellationRequested)
    {
        Thread.Sleep(15);

        int should_have_run =
            stopwatch.ElapsedMilliseconds * taskRateHz / 1000;

        int need_to_run_now = should_have_run - ran_so_far;

        if(need_to_run_now > 0)
        {
             for(int i = 0; i < need_to_run_now; i++)
             {
                 ExecuteTheOperationHere();
             }

             ran_so_far += need_to_run_now;
        }
    }
}

Please note that you want to use longs instead of ints if the process is to remain alive for a very long time.

Community
  • 1
  • 1
Yacoub Massad
  • 27,509
  • 2
  • 36
  • 62
  • 1
    @HenkHolterman, which should be fine since the code will detect that it needs to run the operation more than once and run them immediately to catch up. – Yacoub Massad Apr 24 '16 at 21:25
  • 1
    This means that the times of execution are not uniformly distributed in a perfect way, but its is guaranteed that on average 60 operations run in a second with reasonable accuracy. – Yacoub Massad Apr 24 '16 at 21:27
1

If you replace this:

Task.Delay(Math.Max(0, waitTime)).Wait();

with this:

Thread.Sleep(Math.Max(0, waitTime));

You should get closer values on higher rates (why would you use Task.Delay(..).Wait() anyway?).

Evk
  • 98,527
  • 8
  • 141
  • 191
  • Thanks, that does correct the performance. My library is targeting .Net Platform Standard however so Thread.Sleep is not available to me. – pixsperdavid Apr 24 '16 at 20:52
  • @HenkHolterman I'm also interested why, so far didn't find conclusive answer. But it does "fix" problem mentioned in question (not really, because as you mentioned in comment this is not how it should be done anyway). – Evk Apr 24 '16 at 21:15