简体   繁体   中英

C++ Loop with fixed delta time on a background Thread

The goal is to call a function on a background thread with a fixed delta Time.

The function should get called 60 times / second, hence at timestamps 0, 0.0166, etc. The timestamps should be hit as precisely as possible.

The simple but probably not best solution would be to run a while(true)-loop and let the thread sleep until the next time the function should be called. Here's half C++ / half pseudo-Code how it'd do it.

float fixedDeltaTime = 1.0 / 60.0;
void loopFunction() 
{
      while(true)
      {
         auto currentTime = getTime();
         // do work
         auto timePassed = getTime() - currentTime;
         int times = (timePassed / fixedDeltaTime);
         sleep(  (fixedDeltaTime * times) - timePassed)
      }
}

int main()
{
   std::thread loopFunction(call_from_thread);
   return 0;
}

Is there a better solution than this and would this even work?

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 (eg 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 (eg 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 (eg 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);

Since nobody from the comments gave an example, I'll still post one:

#include <thread>
#include <condition_variable>
#include <mutex>


unsigned int fixedDeltaTime = 1000 / 60;

static std::condition_variable condition_variable;
static std::mutex mutex;

void loopFunction()
{
    while (true)
    {
        auto start = std::chrono::high_resolution_clock::now();
        // Do stuff
        // If you have other means of terminating the thread std::this_thread::sleep_for will also work instead of a condition variable
        std::unique_lock<std::mutex> mutex_lock(mutex);
        if (condition_variable.wait_for(mutex_lock, std::chrono::milliseconds(fixedDeltaTime) - std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start)) == std::cv_status::no_timeout)
        {
            break;
        }
    }
}

int main(int argc, char** argv)
{
    std::thread loopFunction(loopFunction);
    std::this_thread::sleep_for(std::chrono::milliseconds(10000));
    condition_variable.notify_one();
    loopFunction.join();
    return 0;
}

So this uses a condition variable as mentioned initially. It comes in handy to actually tell the thread to stop, but if you have other termination mechanisms then the the locking and waiting can be replaced by a std::this_thread::sleep_for (you should also check if the amount of time is negative to avoid overhead, this example does not perform such check):

#include <thread>

unsigned int fixedDeltaTime = 1000 / 60;

static volatile bool work = false;

void loopFunction()
{
    while (work)
    {
        auto start = std::chrono::high_resolution_clock::now();
        // Do stuff
        std::this_thread::sleep_for(std::chrono::milliseconds(fixedDeltaTime) - std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start));

    }
}

int main(int argc, char** argv)
{
    std::thread loopFunction(loopFunction);
    std::this_thread::sleep_for(std::chrono::milliseconds(10000));
    work = false;
    loopFunction.join();
    return 0;
}

Just a note - this involves querying the clock time from time, which in intensive uses cases can be costly, that is why in my comment I proposed using the platform dependent waitable timer mechanisms where the timer is periodically fired at a given interval and then you only have to wait on it - the wait function will either wait until the next timeout or return immediately if you are late. Windows and POSIX systems both have such timers and I assume they are also available on iOS and OS X. But then you will have to make at least two wrapper classes for this mechanism to use it in a unified way.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM