Reputation: 9841
I'm creating a bunch of threads that need to do work in a frame cycle. I would like to control how many frames are done in a second. I simplified the code I have into this so I can show you what I have wrote
// setup the frame timer
std::chrono::time_point<std::chrono::system_clock> start = std::chrono::system_clock::now();
std::chrono::time_point<std::chrono::system_clock> end = std::chrono::system_clock::now();
while(running == true)
{
// update timer
start = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = start - end;
double frameTime = elapsed_seconds.count();
this->Work(frameTime);
// update timer
std::chrono::time_point<std::chrono::system_clock> afterWork = std::chrono::system_clock::now();
std::chrono::duration<double> elapsedWorkTime = afterWork - end ;
const double minWorkTime = 1000 / this->timer.NumberOfFramePerSeconds;
if(elapsedWorkTime.count() < minWorkTime)
{
double timeToSleep = minWorkTime - elapsedWorkTime.count();
std::this_thread::sleep_for(std::chrono::milliseconds((int)timeToSleep));
}
// update fps
end = start;
timer.FrameCount += 1;
}
Not all threads have an equal amount of work, some have more than others and so without the sleep, I would get results that are around this Thread 1 : 150fps, Thread 2: 5000fps, Thread 3: 5000fps, Thread 4: 5000fps
What I want is to be able to set the frames per second to 60, and so using the code above I would set the this->timer.NumberOfFramePerSeconds
to 60. My problem is that when I do that I end up with a result like this Thread 1 : 30fps, Thread 2: 60fps, Thread 3: 60fps, Thread 4: 60fps
This suggests that there is something wrong with my code as thread 1 will happily do over 150 frames when I comment out the sleep, but when the sleep is there it will work below the 60 I'm trying to achieve.
Is my code/algorithm correct?
Upvotes: 4
Views: 2705
Reputation: 218700
It looks to me that you likely have a units conversion error. This is typically caused by handling units of time outside of the chrono
type system, and introducing explicit conversion factors. For example:
double frameTime = elapsed_seconds.count();
this->Work(frameTime);
Can Work
be modified to take duration<double>
instead of double
? This will help ensure that double
is not reinterpreted as something other than seconds inside of Work
. If for some reason it can not, you can at least reduce your exposure to error with:
this->Work(elapsed_seconds.count());
The following looks very suspicious:
std::chrono::duration<double> elapsedWorkTime = afterWork - end ;
const double minWorkTime = 1000 / this->timer.NumberOfFramePerSeconds;
if(elapsedWorkTime.count() < minWorkTime)
elapsedWorkTime
clearly has units of seconds. minWorkTime
appears to have units of milliseconds. The if
throws away all units information. This should look like:
std::chrono::duration<double> minWorkTime(1./this->timer.NumberOfFramePerSeconds);
if(elapsedWorkTime < minWorkTime)
Or if you really want minWorkTime
to represent milliseconds (which is not necessary):
std::chrono::duration<double, std::milli> minWorkTime(1000./this->timer.NumberOfFramePerSeconds);
if(elapsedWorkTime < minWorkTime)
I recommend the former because the introduction of 1000
is just another chance for an error to creep in.
double timeToSleep = minWorkTime - elapsedWorkTime.count();
std::this_thread::sleep_for(std::chrono::milliseconds((int)timeToSleep));
Here again you are unnecessarily leaving the safety net of chrono
and doing things manually. This should instead simply be:
std::this_thread::sleep_for(minWorkTime - elapsedWorkTime);
Or if you want to be more verbose:
auto timeToSleep = minWorkTime - elapsedWorkTime;
std::this_thread::sleep_for(timeToSleep);
The use of .count()
should be rare. It is currently necessary to use it when printing out duration
s. It can also be necessary to deal with legacy code which can not be made to work with chrono
. If you find yourself using .count()
more than that, step back and try to remove it. chrono
is there to help prevent unit conversion errors, and when you exit the system via .count()
, you subvert chrono
's reason for existing.
Update
VS2013 has a bug such that it won't sleep on double-based durations. So as a workaround...
std::this_thread::sleep_for(std::chrono::duration_cast
<std::chrono::milliseconds>(minWorkTime - elapsedWorkTime));
This is ugly, but still leaves all the conversion factors to chrono
.
Thanks to Caesar for testing this workaround out for me.
Upvotes: 4
Reputation: 47954
Sleeping usually isn't very precise because the resolution of the clock may be low.
For example, on Windows, a typical timer resolution is something like 15.6 ms. If you ask to sleep for just 1 ms, it will sleep until the next 15.6 ms interval that's at least 1 ms in the future. On average, that's 6.8 ms longer than you intended. And once the time does elapse, you've no guarantee that the processor will schedule your thread immediately.
So I suspect that your thread needs to sleep for a short time, but it sleeps significantly longer and misses the next frame. (The fact that the other threads are running at precisely 60 fps suggests that you also have something other rate limiter, like a vertical sync.)
One possible, though inefficient, solution is to do a busy-wait instead of a sleep for any duration that's shorter than the clock resolution. For example, if you have to delay for 3.2*clock_resolution, then you can sleep for 3*clock_resolutions and then spin in a loop until the actual time comes.
Another solution would be to have the threads wait on a synchronization primitive that signals at the desired rate. Depending on the types of primitives you have available, this can be tricky when you're trying to sync up multiple threads.
Upvotes: 1
Reputation: 48247
The simple answer is you don't. See also this SO question.
Instead of controlling framerate, which is error prone and doesn't handle slow threads very well, you should set up your code to handle the current framerate if at all possible.
Let the threads do as much work as they reasonably can. If they have to pause for some reason, use a message queue and sleep when it's empty.
Upvotes: 2