在背景线程上具有固定增量时间的C ++循环

时间:2016-03-09 16:47:50

标签: c++ multithreading c++11 stl thread-safety

目标是在具有固定增量时间的后台线程上调用函数。

该函数应该被调用60次/秒,因此在时间戳0,0.0166等处。应该尽可能精确地命中时间戳。

简单但可能不是最佳解决方案是运行while(true)-loop并让线程休眠直到下次调用该函数。这是半个C ++ /半伪代码它是如何做的。

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;
}

有没有比这更好的解决方案,这甚至会起作用吗?

2 个答案:

答案 0 :(得分:1)

由于评论中没有人给出一个例子,我仍然会发布一个:

#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;
}

因此,这使用最初提到的条件变量。它实际上告诉线程停止是很方便的,但如果你有其他终止机制,那么锁定和等待可以用std::this_thread::sleep_for替换(你还应该检查时间量是否为负值以避免开销,此示例不执行此类检查):

#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;
}

只是一个注意事项 - 这涉及查询时间的时间,在密集的使用情况下可能是昂贵的,这就是为什么在我的评论中我建议使用平台相关的等待计时器机制,其中计时器定期以给定间隔触发然后你只需要等待它 - 等待函数将等到下一个超时或如果你迟到则立即返回。 Windows和POSIX系统都有这样的计时器,我认为它们也可以在iOS和OS X上使用。但是你必须至少为这个机制制作两个包装类才能以统一的方式使用它。

答案 1 :(得分:1)

我提供了一个替代答案,因为我相信这更简单,更准确。首先是代码,然后是解释:

#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();
}

要做的第一件事是创建一个自定义持续时间,完全表示您想要的中断时间:

using delta = std::chrono::duration<std::int64_t, std::ratio<1, 60>>;

delta 完全 1/60秒。

您只需要在线程开头找到一次的当前时间。从那时起,您知道您想要在t + deltat + 2*deltat + 3*delta等处醒来。我已将下一个唤醒时间存储在变量next中:

auto next = std::chrono::steady_clock::now() + delta{1};

现在循环,执行您的操作,然后等待condition_variable,直到时间为next。这可以通过将谓词传递到始终返回wait_until的{​​{1}}来轻松完成。

使用false代替wait_until,您可以放心,您不会慢慢摆脱唤醒时间表。

醒来后,计算下次醒来的时间,然后重复。

有关此解决方案的注意事项:

  • 除了一个地方的1/60规格外,没有人工换算因素。

  • 没有重复调用以获取当前时间。

  • 由于等到未来的时间点,而不是等待持续时间,所以不会因为唤醒时间表而有所偏差。

  • 对计划的精确度没有任何限制(例如毫秒,纳秒,等等)。时间算术是完全。操作系统会将内部精度限制为它可以处理的任何内容。

如果您愿意,还可以使用wait_for代替std::this_thread::sleep_until(time_point)

测量迭代之间的时间

以下是测量迭代之间实际时间的方法。这是上述主题的略微变化。您需要在每个循环中调用condition_variable一次,并记住上一循环中的调用。第一次通过steady_clock::now()将是垃圾(因为没有先前的循环)。

actual_delta

我利用了知道这一事实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}; } } 的所有实现都是steady_clock::duration

nanoseconds

如果有一个实现可以衡量一些可以完全转换为std::chrono::nanoseconds actual_delta = now - prev; 的内容(例如nanoseconds),那么上面的内容仍会编译并继续给我正确的picoseconds个数。这就是为什么我不使用上面的nanoseconds。我想知道我得到了什么。

如果我遇到autosteady_clock::duration更粗糙的实现,或者如果我希望结果更粗糙(例如nanoseconds),那么我会在编译时发现编译时错误。我可以通过选择截断舍入模式来解决该错误,例如:

microseconds

这会将auto actual_delta = std::chrono::duration_cast<std::chrono::microseconds>(now - prev); 转换为now - prev,并在必要时截断。

  

精确的积分转换可能会隐式发生。截断(有损)   转换需要microseconds

Fwiw在实际代码中,在我输入那么多duration_cast之前,我会放弃并写一个本地 using namespace

std::chrono::