3

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?

keyboard
  • 2,137
  • 1
  • 20
  • 32
  • `sleep( (fixedDeltaTime * times) - timePassed)` isn't very precise, it's probably better to use a semaphore or condition variable and wait for notification for a specified time. – πάντα ῥεῖ Mar 09 '16 at 16:50
  • I'd love to have the best possible precision, so if you know how to do it, please add an answer :) – keyboard Mar 09 '16 at 16:55
  • [Here's](http://en.cppreference.com/w/cpp/thread/condition_variable/wait_for) an example. You may use a condition variable to signal the background thread to stop. – πάντα ῥεῖ Mar 09 '16 at 16:59
  • @keyboard what are your requirements ? Posix? Winapi? Can boost be used? – Rudolfs Bundulis Mar 09 '16 at 16:59
  • @RudolfsBundulis Well, tagged as c++11 the standard library comes up with platform independent implementations. – πάντα ῥεῖ Mar 09 '16 at 17:00
  • @πάνταῥεῖ I was just inquiring:) Because if it was WINAPI or Posix specific then a native waitable timer could be used. – Rudolfs Bundulis Mar 09 '16 at 17:01
  • If you can guarantee that loop code itself will not execute more time than `delta`, then you can use std::this_thread::sleep_until. – LibertyPaul Mar 09 '16 at 17:04
  • Thanks for the comments. This code has to run on iOS, Android, OSX, Windows. That's why I wanted to use C++11 platform independant features only. I have no experience with boost and would like to avoid using it if possible. I can not guarantee that loop will not use more time than delta. – keyboard Mar 09 '16 at 17:10
  • @πάνταῥεῖ, you do not have to invoke condvar just to sleep with given precision. sleep_for from `this_thread` gives a good enough precision (that is, the precision which is likely to match precision for condvar sleeping). – SergeyA Mar 09 '16 at 17:16
  • Is it even a good idea to do it like this? Is there maybe even another, better way to do it? – keyboard Mar 09 '16 at 17:32

2 Answers2

4

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);
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • Thank you, this indeed looks very very good and makes a lot more sense. Especially the exact timing looks brilliant! I will try this now and report back. Is there any benefit of using a condition variable instead of sleep_until? Also I don't quite understand what the mutex is needed for / good for. – keyboard Mar 12 '16 at 12:50
  • Oh wait, the lock is used to control the writing on the stop-variable, right? – keyboard Mar 12 '16 at 12:55
  • Okay, I've just tried it and it seems to perform great, thanks again!! Just one more question: How would you measure the time from one loopFunction-call to another with the most possible precision? It can't be guaranteed to always be exactly 1.0/60.0, so I'd like to find out the exact time passed. For that I'm still using chrono with nanoseconds. But maybe you know something better ;) – keyboard Mar 12 '16 at 13:14
  • @keyboard: Yes, I went with the mutex/cv combo so I could easily control the thread life-time. You may or may not need that machinery for your application. I've updated my answer with the info to get actual time between iterations. It is the same as what you had surmised. – Howard Hinnant Mar 12 '16 at 17:34
  • Thanks a lot. This is by far the best and most insightful answer I've ever gotten on SOF. Even if the other answer was correct too, yours has a lot more details and knowledge packed into it, so I marked it as correct. – keyboard Mar 12 '16 at 17:39
  • Okay, so I've got one more question about the conditionVariable and stopping the thread. What happens if the lock_guard is created while the the other thread is not sleeping, hence the other thread is already locking? I've just tried this and it seems to result in a deadlock. – keyboard Mar 13 '16 at 17:45
  • I think I've found the issue: If the work takes so long that there is no wait at all, the thread will never unlock it's mutex when waiting - since it never waits. In this case we can never shutdown the thread. What's the solution to this? – keyboard Mar 13 '16 at 18:01
  • @keyboard: I've wrapped "Do stuff" up in an `mut.unlock()` / `mut.lock()` pair to combat this. Though you must be sure that whatever is done in "Do stuff" does not need to be protected by the mutex. In this simple demo it does not, and this assures that the main thread will have a window to lock the mutex, no matter how lengthy "Do stuff" becomes. Note also that in this scenario (a lengthy "Do stuff"), you will no longer reliably stick to your schedule of an iteration every 1/60s. – Howard Hinnant Mar 13 '16 at 18:32
  • Ah yes, that makes perfect sense. I've tried to do another time measure, and in case "Do Stuff" took to long, skip the current timestamp and go for a later one. Though this needs another time measurement. Your solution might be better! – keyboard Mar 13 '16 at 18:42
1

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.

Rudolfs Bundulis
  • 11,636
  • 6
  • 33
  • 71
  • Thank you, I will check this out. My thread has indeed a way to know if its done / should stop the loop itself. Could you also show how it would look like with std::this_thread::sleep_for - it would be highly appreciated. I'm also open for using pthread, but I was not sure if it works on all platforms without a hassle. That's why I was asking for the C++11 version. – keyboard Mar 09 '16 at 18:12
  • @keyboard added an example with `std::this_thread::sleep_for` and a global volatile flag. – Rudolfs Bundulis Mar 09 '16 at 18:16
  • I've just implemented it and it seems to work (kind of). One more question: In your solution the case of "work taking longer than delta time" is not considered, right? In my pseudo-code there's the times variable for this. Is there any reason you didn't include it? – keyboard Mar 09 '16 at 19:56
  • If the difference between start and end times is larger than the delta the resulting value will be negative and the wait functions should return immediately. But you can just check if the value is negative and run next loop. – Rudolfs Bundulis Mar 09 '16 at 20:14
  • Ah okay, that makes sense. But aren't the values unsigned? Also another question: I've used nanoseconds instead of milliseconds for additional precision and I also set the interval to 1/600 seconds and add up the delta time until it's 1/60 seconds. With 1/60 as the main interval it just didn't get called reliable enough but with 1/600 it works just fine. Though you said there could be performance problems when using the chrono clock too often. Do you think I'm going to run into trouble with this? – keyboard Mar 09 '16 at 20:37