I'm providing an alternative answer as I believe this is both simpler and more accurate. First the code, then the explanation:
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <cstdint>
#include <thread>
static std::condition_variable cv;
static std::mutex mut;
static bool stop = false;
void
loopFunction()
{
using delta = std::chrono::duration<std::int64_t, std::ratio<1, 60>>;
auto next = std::chrono::steady_clock::now() + delta{1};
std::unique_lock<std::mutex> lk(mut);
while (!stop)
{
mut.unlock();
// Do stuff
std::cerr << "working...\n";
// Wait for the next 1/60 sec
mut.lock();
cv.wait_until(lk, next, []{return false;});
next += delta{1};
}
}
int
main()
{
using namespace std::chrono_literals;
std::thread t{loopFunction};
std::this_thread::sleep_for(5s);
{
std::lock_guard<std::mutex> lk(mut);
stop = true;
}
t.join();
}
The first thing to do is to create a custom duration which exactly represents your desired interrupt time:
using delta = std::chrono::duration<std::int64_t, std::ratio<1, 60>>;
delta
is exactly 1/60 of a second.
You only need to find the current time once at the beginning of your thread. From then on you know you want to wake up at t + delta
, t + 2*delta
, t + 3*delta
, etc. I've stored the next wakeup time in the variable next
:
auto next = std::chrono::steady_clock::now() + delta{1};
Now loop, do your stuff, and then wait on the condition_variable
until the time is next
. This is easily done by passing a predicate into wait_until
that always returns false
.
By using wait_until
instead of wait_for
, you are assured that you will not slowly drift off of your schedule of wakeups.
After waking, compute the next time to wake up, and repeat.
Things to note about this solution:
No manual conversion factors, except for the specification of 1/60s in one place.
No repeated calls to get the current time.
No drift off of the schedule of wakeups because of waiting until a future time point, instead of waiting for time duration.
No arbitrary limit to the precision of your schedule (e.g. milliseconds, nanoseconds, whatever). The time arithmetic is exact. The OS will limit the precision internally to whatever it can handle.
There is also a std::this_thread::sleep_until(time_point)
that you could use instead of the condition_variable
if you would prefer.
Measuring time between iterations
Here is how you could measure the actual time between iterations. It is a slight variation on the above theme. You need to call steady_clock::now()
once per loop, and remember the call from the previous loop. The first time through the actual_delta
will be garbage (since there is no previous loop).
void
loopFunction()
{
using delta = std::chrono::duration<std::int64_t, std::ratio<1, 60>>;
auto next = std::chrono::steady_clock::now() + delta{1};
std::unique_lock<std::mutex> lk(mut);
auto prev = std::chrono::steady_clock::now();
while (!stop)
{
mut.unlock();
// Do stuff
auto now = std::chrono::steady_clock::now();
std::chrono::nanoseconds actual_delta = now - prev;
prev = now;
std::cerr << "working: actual delta = " << actual_delta.count() << "ns\n";
// Wait for the next 1/60 sec
mut.lock();
cv.wait_until(lk, next, []{return false;});
next += delta{1};
}
}
I took advantage of the fact that I know that all implementations of steady_clock::duration
are nanoseconds
:
std::chrono::nanoseconds actual_delta = now - prev;
If there is an implementation that measures something that will exactly convert to nanoseconds
(e.g. picoseconds
) then the above will still compile and continue to give me the correct number of nanoseconds
. And this is why I don't use auto
above. I want to know what I'm getting.
If I run into an implementation where steady_clock::duration
is courser than nanoseconds
, or if I want the results in coarser units (e.g. microseconds
) then I will find out at compile-time with a compile-time error. I can fix that error by choosing a truncating rounding mode such as:
auto actual_delta =
std::chrono::duration_cast<std::chrono::microseconds>(now - prev);
This will convert whatever now - prev
is, into microseconds
, truncating if necessary.
Exact integral conversions can happen implicitly. Truncating (lossy)
conversions require duration_cast
.
Fwiw in actual code, I will give up and write a local using namespace
before I type out that many std::chrono::
!
using namespace std::chrono;
auto actual_delta = duration_cast<microseconds>(now - prev);