【问题标题】:Why does Sleep(500) cost more than 500ms?为什么 Sleep(500) 花费超过 500 毫秒?
【发布时间】:2023-03-09 10:08:01
【问题描述】:

我在代码中使用了Sleep(500),并使用getTickCount() 来测试时间。我发现它的成本大约是 515ms,超过 500。有人知道这是为什么吗?

【问题讨论】:

  • getTickCount 的粒度约为 10-16 毫秒。
  • Sleep(n) 并不能保证您的睡眠时间恰好是 nms,只是您的睡眠时间至少 nms。
  • 这个问题在没有 winapi 标签的情况下仍然是相关的。
  • @DavidGrinberg:我很惊讶,你的评论得到了如此多的投票,认为它很有用,而实际上它甚至不是真的。 Sleep 的文档不支持您的主张。具体来说:"如果 dwMilliseconds 小于系统时钟的分辨率,线程可能会休眠小于指定的时间长度。""如果 dwMilliseconds 大于一个刻度但小于比两个,等待可以是任何地方在一到两个滴答之间,依此类推”(强调我的)。
  • 我有一台笔记本电脑,它定期从Sleep 电话返回,大约需要一半的时间,所以那些报告“Sleep 的答案至少保证dwMilliseconds 睡眠”,我什至不同意接着就,随即。我认为它与Intel's SpeedStep 有关,但我不确定在答案中提及这一点。

标签: c++ winapi sleep


【解决方案1】:

正如您在the documentation 中看到的,WinAPI 函数GetTickCount()

受限于系统计时器的分辨率,通常在 10 毫秒到 16 毫秒的范围内。

要获得更准确的时间测量,请使用函数GetSystemDatePreciseAsFileTime

另外,你不能依靠Sleep(500) 来睡眠正好 500 毫秒。它将暂停线程至少 500 毫秒。一旦有可用的时隙,操作系统就会继续该线程。当操作系统上有许多其他任务在运行时,可能会有延迟。

【讨论】:

    【解决方案2】:

    因为 Win32 API 的 Sleep 不是高精度睡眠,并且具有最大粒度。

    获得精确睡眠的最佳方法是少睡一点(约 50 毫秒)并进行忙碌等待。要找到您需要忙等待的确切时间,请使用timeGetDevCaps 获取系统时钟的分辨率,然后乘以 1.5 或 2 以确保安全。

    【讨论】:

    • 正如我们围绕这些部分所说的,“一个人的实时系统就是另一个人的批处理系统”。这完全取决于延迟的可容忍程度,以及延迟的可变性。实时行为通常是一个非常关键的设计决策,不仅仅是从软件 API 的角度来看,还有什么 I/O,它连接到什么(或不连接到,考虑超时),以及它如何影响“实时”潜伏。阻塞调用与异步调用等
    • @franji1 我在回答中没有提到睡眠的另一种用途,它与一种类型的应用程序非常相关:游戏。游戏中的默认设置是忙等到下一帧。这是令人难以置信的功率和 CPU 消耗。如果直到下一帧的时间大于系统滴答时间并且忙于等待其余帧,则只需进行睡眠即可将某些游戏的 CPU 使用率从 100% 降低到 5% 以下。
    • 游戏通常不会以固定的时间间隔呈现。相反,它们尽可能快地渲染,并将场景信息基于当前时间戳。游戏不要使用Sleep
    • @IInspectable 几乎每个游戏都可以选择锁定在 60 或 120 FPS。这些锁的正确实现是睡眠。常见的 vsync 实现阻塞,从而休眠。新的 gsync/freesync 也会阻塞,因此会隐式休眠。
    • 可以选择与显示器刷新率同步的游戏通过使用硬件功能等待垂直空白来实现。他们不使用Sleep
    【解决方案3】:

    sleep(500) 保证睡眠至少 500ms。

    但它的睡眠时间可能会更长:没有定义上限。

    在您的情况下,调用getTickCount() 也会产生额外的开销。

    您的非标准 Sleep 函数可能表现得不同;但我怀疑是否能保证准确性。为此,您需要特殊的硬件。

    【讨论】:

    • MSDN 明确提到了系统时钟滴答的精度,但没有提到未定义的上限。
    • 那又怎样?该平台可能与 MSDN 无关。
    • Sleep 函数不是 C++ 标准函数,它是特定于 Win32 API 的,就像 GetTickCount
    • 标签不能包含大写字母,有争议。另外,据我所知,SleepGetTickCount 的组合仅出现在 Win32 API 中。
    • @orlp 无限期上限的出现是因为没有保证您的进程将在分配的睡眠完成后立即安排。在您再次运行之前,更高优先级的任务可能会占用 CPU 任意时间。也没有保证在你的睡眠完成之前流星不会撞到电脑:)
    【解决方案4】:

    默认计时器分辨率较低,如有必要,您可以增加时间分辨率。 MSDN

    #define TARGET_RESOLUTION 1         // 1-millisecond target resolution
    
    TIMECAPS tc;
    UINT     wTimerRes;
    
    if (timeGetDevCaps(&tc, sizeof(TIMECAPS)) != TIMERR_NOERROR) 
    {
        // Error; application can't continue.
    }
    
    wTimerRes = min(max(tc.wPeriodMin, TARGET_RESOLUTION), tc.wPeriodMax);
    timeBeginPeriod(wTimerRes); 
    

    【讨论】:

    • 与busywaiting相比,这可能不够准确,而且我不会弄乱系统时钟。其他进程可能会搞砸,根据 MSDN:“调用 timeBeginPeriod 时要小心,因为频繁调用会显着影响系统时钟、系统电源使用和调度程序。”.
    • 我之前的评论并不完全准确,timeBeginPeriod 不会搞乱其他进程。
    • @orlp 该建议并不真正适用于运行前台全屏媒体应用程序。它针对诸如 Chrome 之类的违规者。许多应用程序改变了量子并保持不变,即使它们并不真正需要额外的精度。无论如何,这终于解决了(我认为在 Win 7 或 8 中)——系统计时器不再关心量子,它是基于事件的。耶:)
    【解决方案5】:

    一般来说,睡眠意味着您的线程进入等待状态,并且在 500 毫秒后它将处于“可运行”状态。然后 OS 调度程序根据当时可运行进程的优先级和数量来选择运行 something。因此,如果您确实有高精度睡眠和高精度时钟,那么它仍然是至少 500 毫秒的睡眠,而不是 500 毫秒。

    【讨论】:

    • +1 用于提及调度程序以及它会在至少请求的时间内休眠的真正原因。
    • @JordanMelo:除了Sleep“至少在请求的时间内”
    • @IInspectable:对不起,我不是一般的意思。但你是对的:由于系统时钟的粒度,线程的睡眠时间可能比请求的时间略短。我的观点是,即使使用更精细的时钟,您也不能真正依赖准确的睡眠时间。
    • @JordanMelo:粒度与它无关。您可以实现保证睡眠至少请求的时间,无论粒度如何。 Sleep决定不这样做,并且这份合同已记录在案。您仍然是对的,您不能依赖不存在的合同。
    【解决方案6】:

    与其他答案一样,Sleep() 的准确性有限。实际上,no 实现类似Sleep() 的函数可以非常准确,原因如下:

    • 实际调用Sleep() 需要一些时间。虽然以最大准确性为目标的实现可以尝试测量和补偿这种开销,但很少有人打扰。 (而且,在任何情况下,开销都可能因多种原因而有所不同,包括 CPU 和内存使用。)

    • 即使Sleep() 使用的底层计时器在恰好所需时间触发,也不能保证您的进程在唤醒后会立即重新安排。您的进程可能在睡眠时已被换出,或者其他进程可能会占用 CPU。

    • 操作系统可能无法在请求的时间唤醒您的进程,例如因为计算机处于挂起模式。在这种情况下,很可能您的 500 毫秒 Sleep() 通话实际上最终需要几个小时或几天。

    此外,即使Sleep() 非常准确,您想要在睡眠后运行的代码也不可避免地会消耗一些额外的时间。 因此,要定期执行某些操作(例如重绘屏幕或更新游戏逻辑),标准解决方案是使用 compensated Sleep() 循环。也就是说,您维护一个定期递增的时间计数器,指示下一个操作应该何时发生,并将此目标时间与当前系统时间进行比较,以动态调整您的睡眠时间。

    需要特别注意处理意外的大时间跳跃,例如如果计算机暂时被怀疑或如果滴答计数器缠绕,以及处理操作最终花费的时间比下一个操作之前可用的时间更多的情况,导致循环滞后。

    这是一个可以处理这两个问题的快速示例实现(以伪代码形式):

    int interval = 500, giveUpThreshold = 10*interval;
    int nextTarget = GetTickCount();
    
    bool active = doAction();
    while (active) {
        nextTarget += interval;
        int delta = nextTarget - GetTickCount();
        if (delta > giveUpThreshold || delta < -giveUpThreshold) {
            // either we're hopelessly behind schedule, or something
            // weird happened; either way, give up and reset the target
            nextTarget = GetTickCount();
        } else if (delta > 0) {
            Sleep(delta);
        }
        active = doAction();
    }
    

    这将确保每interval 毫秒平均调用一次doAction(),至少只要它不会持续消耗更多时间,并且只要没有发生大的时间跳跃。连续呼叫之间的确切时间可能会有所不同,但任何此类差异都将在下一次交互时得到补偿。

    【讨论】:

      【解决方案7】:

      代码可能需要像“sleep”这样的函数的一般原因有两个:

      1. 它有一些任务可以在未来至少一段距离的任何时间执行。

      2. 它有一些任务应该在未来某个时刻的某个时刻尽可能近地执行。

      在一个好的系统中,应该有不同的方式来发出这些请求; Windows 使第一个比第二个更容易。

      假设系统中有一个 CPU 和三个线程,都在做有用的事情 工作到午夜前一秒,其中一个线程说它不会有 任何有用的事情至少可以做一秒钟。届时,系统将 将执行交给剩下的两个线程。如果在午夜前 1 毫秒, 其中一个线程决定它至少不会有任何有用的事情可做 下一秒,系统会将控制权切换到最后一个剩余线程。

      当午夜临近时,原始的第一个线程将变为可用 运行,但由于当前正在执行的线程将只有 CPU 用于 在那一点上一毫秒,原来的第一个没有特别的原因 线程应该被认为比其他线程更“值得”使用 CPU 时间 刚刚得到控制。由于切换线程不是免费的,操作系统可能非常 很好地决定当前拥有 CPU 的线程应该保留它直到 它阻塞了某些东西或用完了整个时间片。

      如果有一个更易于使用的“睡眠”版本可能会很好 比多媒体定时器,但会要求系统给线程一个 当它有资格再次运行时临时优先级提升,或者更好 “睡眠”的变体,它将指定最短时间和“优先级” 提升”时间,用于需要在特定时间窗口内执行的任务。不过,我不知道有任何系统可以轻松地以这种方式工作。

      【讨论】:

      • 实际上,我认为 Win32 CreateWaitableTimer 的工作方式完全符合您的要求——易于使用并自动提供临时(动态)优先级提升。请参阅MSDN "Priority Boosts" article:“当满足阻塞线程的等待条件时,调度程序会提升线程的优先级。例如,当与磁盘或键盘 I/O 相关的等待操作完成时,线程会收到优先级提升。 "该线程更值得关注,因为它一直在耐心等待。
      猜你喜欢
      • 2017-10-24
      • 2021-08-24
      • 1970-01-01
      • 2019-09-07
      • 1970-01-01
      • 2013-08-11
      • 1970-01-01
      • 1970-01-01
      • 2019-02-14
      相关资源
      最近更新 更多