In many tasks we need to do something at given intervals of time. The most obvious ways may not give you the best results.
Time? Meh.
The most basic tasks that don’t have what you might call CPU-scale time requirements can be handled with the usual language and framework features. These can be things like timers, or simply a call to a sleep(number_of_milliseconds)
function.
void RunTimedThing()
{
while (true)
{
DoMyThing();
sleep(1); // wake up about 1 millisecond later to do it again.
}
}
Exercise 1: How much time is there between each call to
DoMyThing()
?
Most OS thread scheduling is not guaranteed, so your thread isn’t usually executed again exactly when you want it to be. If you are fine with your thing being done again not exactly 1 ms later, this approach is acceptable. But if you need accuracy to the millisecond, or you need it to run more frequently than 1 ms, it is time to level up.
When it positively, absolutely has to be done on time
Now you will need a high resolution timer. These have millisecond or even nanosecond accuracy. It used to be more of a _maybe-you-can-maybe-you-can’t kind of feature feature, but with modern hardware, even the small guys, you usually will be able to get all the way up to nanosecond precision.
For C++, std::chrono
provides such facilities.
// For using 1ms as a const literal for 1 millisecond
using namespace std::chrono_literals;
// For the sake of brevity and sanity
using namespace std::chrono;
// For ease in changing our timing resolution
using Clock = std::chrono::high_resolution_clock;
void RunTimedThingAccurately()
{
const auto kMyThingFrequency = 1ms;
const auto kMaxSleepTime = kMyThingFrequency / 2.0; // set to something less than the frequency
auto nextTime = Clock::now();
while (true)
{
const auto currentTime = Clock::now();
const auto remainingTime = duration_cast(nextTime - currentTime);
if (remainingTime.count() <= 0LL)
{
DoMyThing();
nextTime = currentTime + kMyThingFrequency + remainingTime;
sleepTime = maxSleepTime;
}
else
{
sleepTime = std::min(maxSleepTime, remainingTime);
}
std::this_thread::sleep_for(remainingTime);
}
}
Exercise 2: Now how much time is there between each call to
DoMyThing()
?
Notice that it still isn't enough to just sleep for the interval at which you want to do your thing. Even the higher resolution will have some slop, called jitter, around its timings. To combat that we do a little polling, waking up to see if it is time then sleeping for the remainder of the time until we do our thing. You can try to optimize kMaxSleepTime
to improve accuracy and minimize times being awakened before the work needs to be done.
You may have noticed this subtlety, it is where some magic happens
nextTime = currentTime + kMyThingFrequency + remainingTime;
The remaining time will be negative or zero when this block executes, so it automatically handles being woken up a little late. Otherwise, nextTime
would accumulate those errors.
Another subtlety about kMaxSleepTime
. DoMyThing()
obviously must take less time than kMyThingFrequency
. How long it takes will influence the value you assign to kMaxSleepTime
which needs to be less than the difference between how long your thing takes and how often your thing needs to happen.
A note on std::chrono
The std::chrono
code can be a little ugly without the aliases. But the unit handling is superb. Once you are used to it you may wish to do similar things for other values with units. It is well worth getting comfortable with it when you have to do any time based calculations.