需要精确的线程睡眠。最大 1ms 错误

Posted

技术标签:

【中文标题】需要精确的线程睡眠。最大 1ms 错误【英文标题】:Precise thread sleep needed. Max 1ms error 【发布时间】:2012-11-04 01:20:20 【问题描述】:

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

你有什么建议吗?

更新。 我不能这样做。 在循环结束时,我需要某种睡眠。 我也不想加载 100% 的 CPU。

【问题讨论】:

这是XY problem。无论您实际需要做什么,都可能有一种方法可以做到。但这不是办法。 (否则,如果这确实是您需要做的事情,请将一个核心专用于该线程,然后旋转 5 毫秒。系统在那么短的时间内无法有效地执行其他工作。) “精确到 1 毫秒”有点矛盾。 @JohnDibling:他们要求 Sleep() 延迟 1 毫秒的错误。这不是太难获得。他们也不会将这个词与错误规范一起使用。这有什么矛盾的? @Arno:标题指定误差1ms,问题指定持续时间5ms。这是 20% 的误差。在我的书中,这不是很精确。 @DavidSchwartz:嗯,为了缓存而继续前进并保持对时间片的控制是个好主意,我同意。但是当时间很重要时,它最终对其他线程也很重要。因此,至少尚不清楚通过自旋保持线程运行是否比放弃线程时间片的提醒更好。如今,缓存非常庞大,时间紧迫的应用程序通常不会占用大量内存,尤其是在以 5 ms 的周期重复执行操作时。我什至建议使用Sleep(0) 来改进时间。并且旋转只有在高优先级时才可靠。 【参考方案1】:

我一直在寻找适合实时应用的轻量级跨平台睡眠功能(即具有可靠性的高分辨率/高精度)。以下是我的发现:

调度基础

放弃 CPU 然后将其取回是昂贵的。根据this article 的说法,Linux 上的调度程序延迟可能在 10-30 毫秒之间。因此,如果您需要以低于 10 毫秒的精度睡眠,那么您需要使用特殊的操作系统特定 API。通常的 C++11 std::this_thread::sleep_for 不是高分辨率睡眠。例如,在我的机器上,快速测试表明,当我要求它仅休眠 1ms 时,它通常会休眠至少 3ms。

Linux

最流行的解决方案似乎是 nanosleep() API。但是,如果您想要 alarms。

Windows

这里的解决方案是按照其他人的建议使用多媒体时间。如果你想在 Windows 上模拟 Linux 的 nanosleep(),下面是方法 (original ref)。同样,请注意,如果您在循环中调用 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 和 Apple 平台实现睡眠的time_util.cc。但是请注意,它没有像我上面提到的那样使用 sched_setscheduler 设置实时模式,所以如果你想使用 example here。

#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

另一个更完整的跨平台代码可以是found here。

另一个快速解决方案

您可能已经注意到,上面的代码不再是轻量级的。它需要包含 Windows 标头以及其他内容,如果您正在开发仅标头库,则可能不太理想。如果您需要少于 2 毫秒的睡眠并且您不太热衷于使用操作系统代码,那么您可以使用以下简单的解决方案,它是跨平台的,并且在我的测试中效果很好。请记住,您现在没有使用经过高度优化的操作系统代码,这可能会更好地节省电力和管理 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);
    

相关问题

How to make thread sleep less than a millisecond on Windows Cross platform Sleep function for C++ Is there an alternative sleep function in C to milliseconds?

【讨论】:

如果您关心在系统时钟更改(由人工或 NTP 更改)时睡眠持续时间是否准确,您可能需要std::chrono::steady_clock 而不是high_resolution_clock。否则你的sleep_for() 的睡眠时间可能与预期的完全不同。【参考方案2】:

不要在这里使用旋转。使用标准方法可以达到要求的分辨率准确度。

当系统中断周期设置为以该高频率运行时,您可以使用 Sleep() 直到大约 1 毫秒的周期。查看description of Sleep() 以获取详细信息,尤其是multimedia timers 和Obtaining and Setting Timer Resolution 以获取有关如何设置系统中断周期的详细信息。 如果实施得当,使用这种方法可获得的准确度在几微秒范围内。

我怀疑你的循环也在做其他事情。因此,我怀疑您希望总时间为 5 毫秒,这将是 Sleep() 和您在循环中其他事情上花费的其余时间的总和。

对于这种情况,我建议Waitable Timer Objects,但是,这些计时器也依赖于多媒体计时器 API 的设置。我已经概述了更高精度计时的相关功能here。可以在here 找到对高精度时序的更深入了解。

要获得更准确和可靠的计时,您可能需要查看process priority classesthread priorities。关于 Sleep() 准确性的另一个答案是this。

但是,能否获得精确为 5 毫秒的Sleep() 延迟取决于系统硬件。某些系统允许您以每秒 1024 次中断运行(由多媒体计时器 API 设置)。这对应于 0.9765625 ms 的周期。因此,您可以获得的最接近的是 4.8828125 毫秒。其他允许更接近,特别是自 Windows 7 以来,当在提供high resolution event timers 的硬件上运行时,时间有了显着改善。请参阅 MSDN 上的 About Timers 和 High Precision Event Timer。

总结:设置多媒体定时器以最大频率运行并使用waitable timer。

【讨论】:

我会调查的。谢谢。【参考方案3】:

从问题标签我想你是在 Windows 上。 看看Multimedia Timers,他们宣传的精度低于 1 毫秒。 另一种选择是使用Spin Locks,但这基本上会使cpu核心保持最大使用率。

【讨论】:

实际上,他们并没有宣传低于 1 毫秒的精度。您必须查询受支持的期间范围,然后使用 timeBeginPeriod 到该范围内的某些内容。由于 timeBeginPeriod 采用以毫秒为单位的值,因此您似乎不可能做得比 1 毫秒更好。哦,使用 timeBeginPeriod 加速系统关闭会对系统性能和功耗产生负面影响,因此请务必在不再需要此精度时立即调用 timeEndPeriod。 @AdrianMcCarthy:除了their own docs on "Wait Functions and Time-out Intervals" 声明“如果您调用timeBeginPeriod,请在应用程序的早期调用一次,并确保在应用程序的最后调用timeEndPeriod 函数” 因为“频繁调用会显着影响系统时钟、系统电源使用和调度程序”。因此,如果您在多次调用时都依赖此精度,则不应在每次调用之前和之后进行调整。 并且鉴于 timeBeginPeriodtimeEndPeriod 函数似乎修改了操作系统全局状态(不仅仅是您自己的进程),并且文档似乎暗示 timeBeginPeriod 不是由timeEndPeriod 匹配的即使进程死亡也无法修复,似乎真的很容易(例如,在调整时钟时进行段错误或以其他方式硬杀进程)意外地以系统时钟结束永久处于次优状态(或至少在您重新启动之前)。对于使用电池运行的任何东西来说真的很糟糕,因为增加的电力使用会受到伤害。总的来说,这似乎不是一个好主意。 @ShadowRanger:我很困惑。您似乎同意我写的内容,但写它好像是在反驳。 @AdrianMcCarthy:我只是不同意“一旦你不再需要这个精度就一定要调用 timeEndPeriod”,因为这意味着你可以将它用于细粒度的目的(在睡眠,之后放慢速度),这是明确警告的。我承认,你的措辞有点模棱两可(你的意思可能是“当程序永远再次需要这种精度时”),所以我可能已经过火了。【参考方案4】:

也许你可以尝试一个循环来检查时间间隔并在时间差为 5 毫秒时返回,而不是使用睡眠。循环应该比睡眠更准确。

但是,请注意,精度并不总是可能的。 cpu 可能会在如此小的间隔内被另一个操作占用,并且可能会错过 5 毫秒。

【讨论】:

5ms 并不是一个 非常 小的间隔,虽然 xD 是的,也许我是老派,但它可能会发生,处理器做了其他事情并错过了 1ms 检查。如果 1ms 要求很关键,则应在负载等条件下进行测试。 确实;在这段时间内可以切换一些线程。 blog.tsunanet.net/2010/11/… 这是一个选项。但我想让 CPU 休息 5 毫秒。【参考方案5】:

这些功能:

CreateWaitableTimer SetWaitableTimer WaitForSingleObject

让您创建一个分辨率为 100 纳秒的可等待计时器,等待它,并让调用线程在触发时执行特定函数。

Here's an example of use of said timer.

请注意,WaitForSingleObject 有一个以毫秒为单位的超时,这也许可以作为等待的粗略替代品,但我不相信它。有关详细信息,请参阅此 SO question。

【讨论】:

以上是关于需要精确的线程睡眠。最大 1ms 错误的主要内容,如果未能解决你的问题,请参考以下文章

精确到1ms的定时器

从套接字流读取时是不是需要线程睡眠?

C++ / Qt 减少循环频率

Queue中的Java线程,一旦完成任务,需要休眠

高并发与性能

睡眠方法和多线程的产量方法有什么区别?