需要精确的线程休眠。最大误差为1毫秒。

25

我有一个运行循环的线程。 我需要每5毫秒运行一次该循环(误差为1毫秒)。 我知道Sleep()函数并不准确。

你有什么建议吗?

更新。 我不能用其他方法。 在循环结束时我需要某种形式的Sleep。 我也不想让CPU负载达到100%。


7
这是一个XY问题。无论你实际上需要做什么,都可能有一种方法可以实现。但这不是正确的方式。(否则,如果这确实是你需要做的事情,请为该线程分配一个内核,并自旋5ms。系统无法在这么短的时间内有效地完成其他工作。) - David Schwartz
2
“精确到1毫秒左右”有点自相矛盾。 - John Dibling
1
@JohnDibling:他们要求Sleep()延迟的误差为1毫秒。这并不难实现。而且他们也没有在误差规定中使用“around”这个词。这里有什么矛盾之处? - Arno
1
@Arno:标题指定了1ms的误差,而问题指定了5ms的持续时间。这是20%的误差。在我的书中,这不算很精确。 - John Dibling
1
@DavidSchwartz:为了保持缓存并控制时间片,继续运行是个好主意,我同意。但当时间很重要时,它最终也会影响其他线程。因此,保持线程运行通过自旋是否比放弃其余线程的时间片更好,至少不清楚。现在的缓存非常大,而且时间关键的应用程序通常不占用太多内存,特别是在5毫秒周期内重复执行时。我甚至建议使用Sleep(0)来改善时间控制。而且自旋只在高优先级下可靠。 - Arno
显示剩余5条评论
6个回答

37

我正在寻找一种适用于实时应用程序的轻量级跨平台睡眠函数(即具有高分辨率/高精度和可靠性)。这是我的研究结果:

调度基础知识

放弃CPU并重新获取它是昂贵的。根据这篇文章,Linux上的调度延迟可能在10-30ms之间。因此,如果您需要高精度地睡眠不到10ms,则需要使用特殊的操作系统特定API。通常的C ++ 11 std :: this_thread :: sleep_for不是高分辨率睡眠。例如,在我的计算机上,快速测试表明,当我要求它仅睡眠1ms时,它经常睡眠至少3ms。

Linux

最流行的解决方案似乎是nanosleep()API。但是,如果您想要< 2ms的高分辨率睡眠,则还需要使用sched_setscheduler调用将线程/进程设置为实时调度。如果不这样做,则nanosleep()的行为就像过时的usleep,其分辨率约为10ms。另一种可能性是使用alarms

Windows

这里的解决方案是使用多媒体时间,正如其他人建议的那样。如果您想在Windows上模拟Linux的nanosleep(),请参考下面的方法(原始参考)。同样,请注意,如果您正在循环调用sleep(),则无需一遍又一遍地进行CreateWaitableTimer()。

#include <windows.h>    /* WinAPI */

/* Windows sleep in 100ns units */
BOOLEAN nanosleep(LONGLONG ns){
    /* Declarations */
    HANDLE timer;   /* Timer handle */
    LARGE_INTEGER li;   /* Time defintion */
    /* Create timer */
    if(!(timer = CreateWaitableTimer(NULL, TRUE, NULL)))
        return FALSE;
    /* Set timer properties */
    li.QuadPart = -ns;
    if(!SetWaitableTimer(timer, &li, 0, NULL, NULL, FALSE)){
        CloseHandle(timer);
        return FALSE;
    }
    /* Start & wait for timer */
    WaitForSingleObject(timer, INFINITE);
    /* Clean resources */
    CloseHandle(timer);
    /* Slept without problems */
    return TRUE;
}

跨平台代码

这里是实现Linux、Windows和苹果平台上的sleep函数的time_util.cc代码。需要注意的是,它并未像我之前提到的那样使用sched_setscheduler设置实时模式,所以如果你想要使用小于2毫秒的时间,则需要进行额外的设置。另一个可以改进的地方是,在某些循环中反复调用CreateWaitableTimer会对Windows版本产生性能影响,你可以参考这里的示例来避免这种情况。

#include "time_util.h"

#ifdef _WIN32
#  define WIN32_LEAN_AND_MEAN
#  include <windows.h>

#else
#  include <time.h>
#  include <errno.h>

#  ifdef __APPLE__
#    include <mach/clock.h>
#    include <mach/mach.h>
#  endif
#endif // _WIN32

/**********************************=> unix ************************************/
#ifndef _WIN32
void SleepInMs(uint32 ms) {
    struct timespec ts;
    ts.tv_sec = ms / 1000;
    ts.tv_nsec = ms % 1000 * 1000000;

    while (nanosleep(&ts, &ts) == -1 && errno == EINTR);
}

void SleepInUs(uint32 us) {
    struct timespec ts;
    ts.tv_sec = us / 1000000;
    ts.tv_nsec = us % 1000000 * 1000;

    while (nanosleep(&ts, &ts) == -1 && errno == EINTR);
}

#ifndef __APPLE__
uint64 NowInUs() {
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    return static_cast<uint64>(now.tv_sec) * 1000000 + now.tv_nsec / 1000;
}

#else // mac
uint64 NowInUs() {
    clock_serv_t cs;
    mach_timespec_t ts;

    host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cs);
    clock_get_time(cs, &ts);
    mach_port_deallocate(mach_task_self(), cs);

    return static_cast<uint64>(ts.tv_sec) * 1000000 + ts.tv_nsec / 1000;
}
#endif // __APPLE__
#endif // _WIN32
/************************************ unix <=**********************************/

/**********************************=> win *************************************/
#ifdef _WIN32
void SleepInMs(uint32 ms) {
    ::Sleep(ms);
}

void SleepInUs(uint32 us) {
    ::LARGE_INTEGER ft;
    ft.QuadPart = -static_cast<int64>(us * 10);  // '-' using relative time

    ::HANDLE timer = ::CreateWaitableTimer(NULL, TRUE, NULL);
    ::SetWaitableTimer(timer, &ft, 0, NULL, NULL, 0);
    ::WaitForSingleObject(timer, INFINITE);
    ::CloseHandle(timer);
}

static inline uint64 GetPerfFrequency() {
    ::LARGE_INTEGER freq;
    ::QueryPerformanceFrequency(&freq);
    return freq.QuadPart;
}

static inline uint64 PerfFrequency() {
    static uint64 xFreq = GetPerfFrequency();
    return xFreq;
}

static inline uint64 PerfCounter() {
    ::LARGE_INTEGER counter;
    ::QueryPerformanceCounter(&counter);
    return counter.QuadPart;
}

uint64 NowInUs() {
    return static_cast<uint64>(
        static_cast<double>(PerfCounter()) * 1000000 / PerfFrequency());
}
#endif // _WIN32

这里可以找到另一个更完整的跨平台代码:链接

另一个快速解决方案

你可能已经注意到,上面的代码不再是非常轻量级了。它需要包括Windows头文件以及其他一些可能并不理想的内容,特别是当你在开发仅有头文件的库时。如果你需要的睡眠时间小于2ms,并且你不是非常喜欢使用操作系统代码,那么你可以使用以下简单的解决方案,这是跨平台的,并且在我的测试中表现非常好。只需记住,现在你没有使用严重优化的操作系统代码,后者可能更擅长节省电力和管理CPU资源。

typedef std::chrono::high_resolution_clock clock;
template <typename T>
using duration = std::chrono::duration<T>;

static void sleep_for(double dt)
{
    static constexpr duration<double> MinSleepDuration(0);
    clock::time_point start = clock::now();
    while (duration<double>(clock::now() - start).count() < dt) {
        std::this_thread::sleep_for(MinSleepDuration);
    }
}

相关问题


1
如果您关心系统时钟被人为或NTP更改时睡眠持续时间的准确性,那么您可能想使用std::chrono::steady_clock而不是high_resolution_clock。否则,您的sleep_for()可能会睡眠比预期的时间长得多。 - John Zwinck

20

请不要使用旋转。使用标准方法可以达到所需的分辨率准确度。

当系统中断周期设置为高频时,您可以将Sleep()使用至约1毫秒的时间段。请参阅Sleep()的说明以获取详细信息,尤其是使用多媒体定时器获取和设置计时器分辨率来获取有关如何设置系统中断周期的详细信息。 如果正确实现此方法,则可达到几微秒的可获得精度

我怀疑您的循环还在做其他事情。因此,我怀疑您想要总周期为5毫秒,这将是您在循环中使用Sleep()以及其他时间的总和。

对于这种情况,我建议使用可等待计时器对象,但是,这些计时器也依赖于多媒体定时器API的设置。我在这里概述了高精度定时相关函数的相关内容。有关高精度时间的更深入洞察力可以在此处找到。

要获得更准确和可靠的定时,您可能需要查看进程优先级类线程优先级。关于Sleep()的精度的另一个答案在这里

然而,能否精确获得5毫秒的Sleep()延迟取决于系统硬件。一些系统允许您以每秒1024个中断的速度运行(由多媒体定时器API设置)。这对应一个周期为0.9765625毫秒。因此,最接近的时间是4.8828125毫秒。其他一些系统允许更接近的时间,特别是在提供高分辨率事件定时器的硬件上,自Windows 7以来计时显著改进。请参见MSDN上的有关定时器的信息高精度事件定时器

总结:将多媒体定时器设置为最大频率并使用可等待定时器


9

根据问题标签,我猜你是在使用Windows系统。可以看一下Multimedia Timers,它们可以提供高于1毫秒的精度。

另一个选择是使用自旋锁,但这基本上会使CPU核心保持最大使用率。


3
事实上,它们并没有宣传精度低于1毫秒。您需要查询支持的周期范围,然后使用timeBeginPeriod设置该范围内的值。由于timeBeginPeriod接受以毫秒为单位的值,因此似乎很难做到比1毫秒更好。噢,而且通过使用timeBeginPeriod加速系统会对系统性能和功耗产生负面影响,因此请确保在不再需要这种精度时立即调用timeEndPeriod。 - Adrian McCarthy
2
@AdrianMcCarthy: 除了它们自己的“等待函数和超时间隔”文档指出“如果调用了timeBeginPeriod,则应在应用程序早期调用一次,并确保在应用程序结束时调用timeEndPeriod函数”,因为“频繁的调用会显著影响系统时钟、系统功率使用和调度程序”。因此,如果您依赖于这种精度进行多次调用,则不应在每次调用之前和之后调整。 - ShadowRanger
2
考虑到 timeBeginPeriodtimeEndPeriod 函数似乎修改操作系统全局状态(而不仅仅是您自己的进程),并且文档似乎暗示一个没有被 timeEndPeriod 匹配的 timeBeginPeriod 甚至在进程死亡后也无法恢复,因此很容易(例如在调整时钟时发生段错误或以其他方式强制终止进程)意外地使系统时钟永久处于次优状态(或者至少直到您重新启动)。对于任何运行在电池上的设备来说都非常糟糕,因为增加的功耗会损害电池寿命。总的来说,这似乎不是一个好主意。 - ShadowRanger
@ShadowRanger:我很困惑。你似乎在赞同我写的内容,但却写得好像是在反驳一样。 - Adrian McCarthy
@AdrianMcCarthy:我只是不同意“一定要在你不再需要这个精度时立即调用timeEndPeriod”,因为这暗示你可能会将它用于细粒度目的(在睡眠前加速时钟,在之后减慢它),而这是明确警告过的。我承认,你的措辞有点模糊(你可能的意思是“当程序再也不需要那种精度时”),所以我可能有点草率。 - ShadowRanger
@ShadowRanger:我仍然不确定我看到争议的重点。考虑一个需要高精度运行短动画但只偶尔需要显示这样动画的应用程序。我希望它在动画开始之前不会增加时钟精度,并且在动画结束后立即恢复旧的精度。因为它可能稍后需要显示另一个动画而保持精度很高并不是很友好。 - Adrian McCarthy

4

与其使用sleep,也许您可以尝试循环检查时间间隔,并在时间差为5毫秒时返回。该循环应该比sleep更准确。

然而,请注意精度并不总是可能的。CPU 可能会被另一个操作捆绑,因此可能会错过5毫秒。


5
5毫秒并不是一个非常短的时间间隔,尽管xD。 - SingerOfTheFall
1
是的,也许我有点守旧,但这种情况确实可能发生,处理器可能会执行其他操作而错过1毫秒的检查。如果1毫秒的要求至关重要,就应该在负载下进行测试等。 - Kami
确实,在那么短的时间内可以切换几个线程。http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html - Rook
这是一个选项。但我想让CPU休息5毫秒。 - Hooch

3
这些函数: 让您可以创建一个具有100纳秒分辨率的可等待定时器,并等待它,在触发时间时调用特定函数的调用线程。 以下是使用该定时器的示例
请注意,WaitForSingleObject的超时时间以毫秒为单位测量,这可能可以作为等待的粗略替代,但我不会信任它。有关详细信息,请参阅此SO 问题

1
如果您需要精确度,并且拥有管理员访问权限,在Linux上,您可以通过read()调用来设置时钟芯片进行轮询。操作系统将会阻塞直到下一个信号从芯片(中断)进入。
当我拥有对于第二个RTC的访问权时,我曾经成功地使用过这种方法,例如'/dev/rtc2'。此外,它也不需要额外的电力消耗。
在Windows上,您可能可以创建一个用户模式驱动程序来完成同样的事情。或者,QueryPerformanceCounter()是高分辨率计时器,建议使用它而不是使用'rdtsc'汇编指令。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接