是否可以仅使用标准 c++/c++11 实现不带"sleep"的计时器?

Can you implement a timer without a "sleep" in it using standard c++/c++11 only?

本文关键字:实现 sleep 计时器 c++11 c++ 标准 是否      更新时间:2023-10-16

重要更新

注意:由于这个问题是专门关于定时器的,因此需要注意的是,gcc中有一个错误,即如果您使用std::condition_variable::wait_fo(或wait_util),即使您给它传递std::chrono::steady_clock时间点,它也会使用系统时钟。这意味着计时器不是单调的——也就是说,如果你将系统时间向前更改一天,那么你的计时器可能不会触发一天+你的超时——如果你将时间向后更改,你的计时器可以立即触发。

请参阅:等待直到系统时间更改的condition_variable变通方法

这个错误的修复程序进入了gcc v10+

结束

我有以下代码(手工复制):

// Simple stop watch class basically takes "now" as the start time and 
// returns the diff when asked for.
class stop_watch {...}
// global var
std::thread timer_thread;
void start_timer(int timeout_ms)
{
timer_thread = std::thread([timeout_ms, this](){
stop_watch sw;
while (sw.get_elapsed_time() < timeout_ms)
{
// Here is the sleep to stop from hammering a CPU
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// Do timeout things here...
std::cout << "timed out!" << std::endl;
})
}

我不想太拘泥于我写的课的细节,所以这是一个非常精简的版本。完整类调用一个函数回调,并有一个变量来取消计时器等。

我只是想专注于";睡眠;部分我能在不睡觉的情况下实现这样的东西吗?或者有更好的方法吗?-还是睡眠很好?-我认为睡眠通常是糟糕设计的标志(我在一些地方读过)。。。但我想不出一种没有定时器的方法来实现定时器:(

附加说明:计时器应具有能够随时停止/唤醒的要求。只是为了清楚起见,添加了这一点,因为它似乎会影响选择什么样的解决方案。在我的原始代码(而不是这个片段)中,我使用了一个可以脱离循环的原子布尔标志。

C++11为我们提供了std::condition_variable。在你的计时器中,你可以等待,直到你的条件得到满足:

// Somewhere else, e.g. in a header:
std::mutex mutex;
bool condition_to_be_met{false};
std::condition_variable cv;
// In your timer:
// ...
std::unique_lock<std::mutex> lock{mutex};
if(!cv.wait_for(lock, std::chrono::milliseconds{timeout_ms}, [this]{return condition_to_be_met;}))
std::cout << "timed out!" << std::endl;

您可以在此处找到更多信息:https://en.cppreference.com/w/cpp/thread/condition_variable

要发出条件已满足的信号,请在另一个线程中执行此操作:

{
std::lock_guard<std::mutex> lock{mutex}; // Same instance as above!
condition_to_be_met = true;
}
cv.notify_one();

虽然您的代码可以"工作",但对于定时器的预期用途来说,它是次优的。

存在std::this_thread::sleep_until,根据实现的不同,可能在做了一些数学运算后才调用sleep_for,但可以使用适当的计时器,这在精度和可靠性方面非常优越。

一般来说,睡觉不是最好、最可靠、最准确的事情,但有时,如果只是等待一些大致的时间,那就"足够好"了。

无论如何,像你的例子中那样,重复少量睡眠是个坏主意。这将在不必要的重新调度和唤醒线程时消耗大量CPU,在某些系统上(尤其是Windows,尽管Windows 10在这方面已经不那么糟糕了),可能会增加相当大的抖动和不确定性。请注意,不同的Windows版本对调度程序的粒度的调整方式不同,因此,除了通常不会过于精确之外,您甚至没有一致的行为。舍入在很大程度上是"谁在乎"一个大的等待,但对于一系列小的等待来说,这是一个严重的问题。

除非有必要提前中止计时器(但在这种情况下,也有更好的方法来实现这一点!),否则您应该在整个持续时间内只睡一次,而不是更多。为了确保正确性,您应该检查您是否确实获得了预期的时间,因为有些系统(尤其是POSIX)可能处于睡眠状态。

如果你需要的话,过度睡眠是一个不同的问题,因为即使你正确地检查和检测了这种情况,一旦发生,你也无能为力(时间已经过去了,再也不会回来了)。但遗憾的是,这只是睡眠的一个根本弱点,你无能为力。幸运的是,大多数人在大多数时候都能摆脱这个问题。

可以在循环中忙于等待检查时间,直到它达到您等待的时间。这显然很可怕,所以不要这么做。睡10毫秒有点好,但绝对是糟糕的设计。(@Damon的回答有一些好的信息。)


如果以sleep的名义使用函数对您的程序来说是最有用的,那么这并没有错

避免sleep的建议可能是反对短时间睡眠的一般模式,检查是否有什么事情可做,然后再睡。相反,阻止等待一个没有超时或超时时间很长的事件。(例如,GUI应该通过调用阻塞函数来等待按键/点击,并设置一个超时,以便在自动保存或下一步发生的任何事情时将其唤醒。您通常不需要单独的线程来睡眠,但如果没有合理的插入当前时间的检查,则可以。)

当终于到了做任何事情的时候,让操作系统唤醒你会更好。这样可以避免上下文切换和缓存污染,从而在睡眠不足时减慢其他程序的运行速度并浪费电源

如果你知道一段时间内没有什么可做的,就睡那么久,只睡一次。AFAIK,在Windows、Linux或OS X等主流操作系统上,多次短暂睡眠不会提高唤醒时间的准确性。如果你的代码经常唤醒,你可能会避免指令缓存丢失,但如果延迟量是一个真正的问题,你可能需要一个实时操作系统和一种更复杂的计时方法。(比如提前一秒钟醒来,然后旋转等待。)

如果有什么不同的话,一个睡眠了很长时间的线程更有可能在它请求的时候醒来,而一个最近运行但只睡了10ms的线程可能会遇到调度程序时间片问题。在Linux上,休眠了一段时间的线程在醒来时会获得优先级提升。


以阻止1秒的名称使用不带sleep的函数并不比使用sleepthis_thread::sleep_for好。

(显然,你希望另一个线程能够唤醒你。这个要求被隐藏在问题中,但是的,条件变量是一个很好的可移植方法。)


如果您想使用纯ISO C++11,那么std::this_thread::sleep_forstd::this_thread::sleep_until是您的最佳选择。它们在标准标头<thread>中定义。

sleep(3)是POSIX函数(与nanosleep类似),而不是ISO C++11的一部分。如果这对你来说不是问题,那么如果合适的话,可以随意使用。


对于一段时间内的便携式高精度操作系统辅助睡眠,C++11引入了
std::this_thread::sleep_for(const std::chrono::duration<Rep, Period> &sleep_duration)(cppreference页面有一个使用它的代码示例。)

阻止的当前线程执行至少指定的sleep_duration。

由于以下原因,此函数可能会阻塞超过sleep_duration的时间调度或资源争用延迟。

该标准建议使用稳定的时钟来测量期间如果实现使用系统时钟,则等待时间也可能对时钟调整敏感。


要休眠直到时钟到达指定时间(可能考虑到系统时间的更改/校正):

std::this_thread::sleep_until(const std::chrono::time_point<Clock,Duration>& sleep_time)

在指定sleep_time之前阻止当前线程的执行已联系到。

使用绑定到sleep_time的时钟,这意味着时钟被考虑在内。因此,块的持续时间可能,但可能不会,小于或大于sleep_time - Clock::now()在呼叫时,取决于调整的方向。该功能也可以阻塞超过sleep_time之后的时间由于调度或资源争用延迟而达到。


请注意,sleep_for不受系统时钟变化的影响,因此它会实时休眠。

但是sleep_until应该让你在系统时钟到达给定时间时醒来,即使它是通过调整(NTP或手动设置)来实现的,如果与steady_clock以外的时钟一起使用。


睡眠问题:晚睡/早起

当然,关于可能睡得太久的警告也适用于sleepnanosleep,或者任何其他特定于操作系统的睡眠或超时(包括@Sebastian回答中的条件变量方法)。这是不可避免的;不过,实时操作系统可以为您提供额外延迟的上限。

你已经在用10毫秒的睡眠做这样的事情了:

始终假设sleep或任何其他功能醒来较晚,并检查当前时间,而不是在任何重要的情况下使用航位推算

您不能从重复的sleep中构建可靠的时钟。例如,不要建立一个倒计时计时器,它会休眠1秒,递减并显示计数器,然后再休眠一秒钟。除非它只是一个玩具,而且你不太关心准确性。

一些睡眠功能,如POSIXsleep(3),也可以在收到信号后提前醒来。如果过早醒来是一个正确性问题,请检查时间,如有必要,在计算的时间间隔内重新入睡。(或使用sleep_until)