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.
using namespace std::chrono_literals; // For using 1ms as a const literal for 1 millisecondusing namespace std::chrono; // For the sake of brevity and sanityusing Clock = std::chrono::high_resolution_clock; // For ease in changing our timing resolutionvoid 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<microseconds>(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.