【问题标题】:.NET Thread.Sleep() is randomly imprecise.NET Thread.Sleep() 随机不精确
【发布时间】:2012-09-07 15:34:47
【问题描述】:

在我的 .NET 应用程序中,我必须重放一系列传感器事件。所以我创建了一个线程来触发这些事件(通常大约每 1-4 毫秒)。我在这个线程中实现了一个循环,并使用Thread.Sleep(...) 让线程在事件之间休眠。

基本上是这样的:

void RunThread() {
  var iter = GetAllEvents().GetEnumerator();
  if (!iter.MoveNext()) {
    return;
  }

  DateTime lastEventTime = iter.Current.Timestamp;
  FireEvent(iter.Current);

  while (iter.MoveNext()) {
    MyEvent nextEvent = iter.Current;

    int timeout = (int)(nextEvent.Timestamp - lastEventTime).TotalMilliseconds; 
    Thread.Sleep(timeout);

    FireEvent(nextEvent);
    lastEventTime = nextEvent.Timestamp;
  }
}

现在,我的问题是 Thread.Sleep() 有时会遵守指定的超时时间,有时则不会。

我添加了一个检查(使用StopWatch,在上面的代码中不可见)关于睡眠实际花费了多长时间。以下是一些结果:

Expected timeout of 1 ms but got 15 ms.
Expected timeout of 2 ms but got 13 ms.
Expected timeout of 3 ms but got 15 ms.
Expected timeout of 2 ms but got 13 ms.
Expected timeout of 1 ms but got 13 ms.
Expected timeout of 1 ms but got 15 ms.
Expected timeout of 2 ms but got 13 ms.
Expected timeout of 2 ms but got 40 ms.

为什么Thread.Sleep() 表现得那样“随机”?

注意事项:

  • 该线程运行时,Window 任务管理器不显示任何 cpu 使用情况。
  • 行为随机变化,但至少会持续几秒钟。例如,有一次我启动线程时,它运行良好。下一次它一直运行缓慢。下一次它快速运行几秒钟,然后减速或相反。

更新:下面是一些伪代码,显示了Sleep 的行为方式:

bool ChooseRespectTimeout() {
  if (this.notYetChosen) {
    this.respectTimeout = ChooseRandomly()
    this.notYetChosen = false
    reset this.notYetChosen after random time period
  }
  return this.respectTimeout
}

void Sleep(int timeout) {
  if (ChooseRespectTimeout())
    Thread.Sleep(timeout)
  else
    Thread.Sleep(timeout * 10)
}

【问题讨论】:

  • 我不知道他们为什么没有更新该方法的文档。该方法实际上只是调用 Sleep msdn.microsoft.com/en-us/library/windows/desktop/… 详细说明缺乏准确性。
  • Thread.Sleep 真正做的是放弃控制 大约 指定的毫秒。那可能或多或少。操作系统交还控制权的速率由系统时间片决定,即 10-15 毫秒。所以,实际上Thread.Sleep 只能精确到最近的量子。但是,正如其他人指出的那样,如果 CPU 负载过重,则时间会更长。此外,如果您的线程的优先级低于所有其他线程,则可能无法获得时间。
  • 长话短说,只使用Thread.Sleep 让其他应用程序/线程有一个时间片,并且只使用 Thread.Sleep(1) 来做到这一点。 bit.ly/IhxHSk 即不要使用Thread.Sleep 进行计时。
  • 我已经改变了我的实现。我仍然很好奇它为什么会这样。就像我在问题中所说的那样,没有明显的 CPU 负载(所以这不是问题)。此外,线程具有最高优先级,仍然表现出这种奇怪的行为。
  • 顺便说一句,这不是现在Sleep 的行为。 Sleep 选择在时间到期(或接近)后不交还控制权。它不能选择不“尊重”超时 Sleep 被调用。

标签: .net multithreading


【解决方案1】:

Thread.Sleep 真的只是调用 win32 Sleep function,其文档详细说明了不准确之处。

Thread.Sleep 真正做的是在 大约 指定的毫秒内放弃控制。 Sleep 函数细节可能或多或少。操作系统交还控制权的速率由系统时间片决定,即 10-15 毫秒。所以,实际上Thread.Sleep 只能精确到最近的量子。但是,正如其他人指出的那样,如果 CPU 处于重负载下,时间会更长。另外,如果您的线程的优先级低于所有其他线程,则可能无法获得时间。

就负载而言,它与可能发生切换时的负载有关。您说的是 1ms 超时(实际上大约是 15,正如您所见证的,由于系统量子或计时器的准确性),这意味着操作系统实际上只需要忙碌 15-30 毫秒就可以了太忙了,无法将控制权交还给线程,直到未来的量子。这不是很多时间。如果在这 15 到 40 毫秒内有更高优先级的东西需要 CPU,则更高优先级的线程会获得时间片,可能会使放弃其控制权的线程饿死。

Thread.Sleep 的真正含义(对于大于 1 的超时值)是它告诉操作系统该线程的优先级低于系统中的所有其他线程至少对于 超时值,直到线程重新获得控制权。 Thread.Sleep 不是一种计时机制,它是一种放弃控制的手段。他们真的应该将“Sleep”重命名为“Yield”。

如果你想设计一些周期性地做某事的东西,请使用计时器。如果您需要非常精确,请使用多媒体计时器。

【讨论】:

  • 我一直将它用作间隔计时器,但对于任何接近精确的亚秒间隔的东西都没有,(因为它不起作用 - 正如我们所知)。
  • @MartinJames 你为什么要浪费一个线程,它是 1 兆字节的堆栈? Timer 类有什么问题?
  • 需要另一个线程来操作定时器。更多上下文交换。 1MB 堆栈是默认值,而不是锁。当堆栈上有多个功能/层时,计时器信号更难以实现。简单。调用线程无论如何都存在。消除状态引擎。开箱即用。不需要更多内容时的琐碎输入。适用于所有多任务操作系统。
  • 在 Elapsed 事件期间只需要另一个线程,而不是整个 60 秒。如果这是调用 Sleep 的线程池线程,它也会强制线程池创建一个新线程,因为该线程被阻塞 Sleep。如果你想让它过于复杂,那么所有其他的东西都适用于任何一种情况——或者根本不适用。 “琐碎的打字”?
【解决方案2】:

Windows 不是实时操作系统。

调度程序可能等待比请求的睡眠持续时间更长的时间来执行一个线程,尤其是当另一个线程仍然忙碌时。

如果您想要更高的可靠性,请尝试使用多媒体计时器。有一个 .NET 包装器 here

【讨论】:

    【解决方案3】:

    不保证您的程序是 CPU 的唯一用户。

    其他进程获得 CPU 切片 - Thread.Sleep 将在超时后做出最佳尝试恢复,但如果 CPU 忙,则必须等待。

    【讨论】:

    • 但为什么它的行为如此随机,却在更长的时间内保持其随机选择的行为?
    • 不,它不必等待,如果您将睡眠线程提高到比所有其他线程更高的优先级,则不必等待。
    【解决方案4】:

    Thread.Sleep 只保证线程将休眠至少指定的时间间隔。它不提供准确性保证——实际上它不能这样做,因为它是操作系统调度程序决定何时应该为每个阻塞线程分配 CPU 时间。

    如果线程可以要求它们在特定时间点再次运行,那么抢占式多任务处理将是不可能的。

    【讨论】:

    • 它甚至似乎也没有在至少指定的时间间隔内休眠。它对我来说很早醒来,即睡 1 秒通常会在 980 毫秒左右后醒来。
    【解决方案5】:

    Windows 不是实时操作系统,因此您无法获得此类保证。基本上,Thread.Sleep 将休眠 至少 毫秒,但它可能会更长,尤其是对于非常小的值。

    【讨论】:

    • 这仍然不能回答为什么它(仅)有时有效。如果它有效,它会在更长的时间内有效。如果没有,则在更长的时间段内无法正常工作。这不像是每隔几毫秒就会切换一次。
    【解决方案6】:

    Thread.Sleep() 不准确。您给它的数字是MINIMUM 睡眠时间,而不是确切的睡眠时间。此外,Thread.Sleep 还有一个最小粒度,我认为是 10ms IIRC 左右。

    如果您需要更精确的计时,那么您可能想要使用计时器。但是,请注意,尤其是 Windows 和 .NET 不是“实时”的,因此它们无法为您提供精确的准确性。

    例如,如果垃圾收集器启动,处理计时器可能需要更长的时间。

    【讨论】:

    • 但是如果粒度是 10 毫秒,为什么 Thread.Sleep() 有时会精确呢?
    【解决方案7】:

    您不是最近唯一指出这一点的人。自 XP 以来,一定发生了一些变化。 Sleep() 然后在有用的时间间隔内非常可靠,并且确实准确,例如。秒或分钟。正如其他人指出的那样,4ms 完全超出了 Sleep() 可用的时间。

    现在看来,随着后来的操作系统,一些额外的不确定性不知何故潜入了,虽然我自己没有测试过。

    在有用的长时间内,您应该提高睡眠线程的优先级,以便在睡眠间隔到期前准备好后立即调度它。

    对于 4 毫秒,查看多媒体计时器,正如科幻小说 (+1) 所建议的那样。

    【讨论】:

    • Sleep 从来都不是准确的,即使到下一个量程也是如此。您可以运行确实在一小段时间内始终达到最近量子点的代码;但这并不能使它在调用时更准确,甚至是 XP ......这并不像 Sleep 函数的文档自 XP 以来真的发生了变化。 Joe Duffy 早在 2006 年 8 月 (bit.ly/RICHau) 就谈到了 Sleep,这是基于 XP 的经验和知识,因为 Vista 尚未发布。
    • @PeterRitchie - 当然,小睡眠是无可救药的不稳定,但是 XP 上的 Sleep(60000) 循环是一个合理的分钟计时器。当然,随着时间的推移,由于计时器粒度导致的溢出会增加,但分钟总是大约 60 秒 。有几篇文章表明这​​种行为已经改变,并且涉及到一些额外的因素。自 XP 以来我没有自己测试过,所以我只有轶事。
    • 这是合理的分钟计时器,因为如果它关闭 15 或事件 40 毫秒,没关系。这不是准确性的差异。它仍然关闭了 15-40 毫秒...
    • 嗯,我认为 60 秒以上比 6 毫秒以上更准确是合理的!
    • 不,超过 60 秒的百分比误差小于 6ms,它不是更“准确”。它仍然关闭 15-40 毫秒(或任何数字)。准确度定义为“测量值与真实值或可接受值的接近程度”。如果 Sleep(6) 导致 15ms 后重新获得控制权,Sleep(60000) 导致 60009 ms 后重新获得控制权,则准确度相同:+9ms;
    猜你喜欢
    • 2021-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多