线程休眠(Thread.Sleep(1))需要的时间比1毫秒长。

10

我搜索了这个问题,但没有找到答案。如果有重复的话,我会很乐意关闭它。

我目前正在尝试对一项技术进行性能评估,看到了一些令人难以置信的结果,所以决定进行一些实验。在其中,我想尝试并查看Stopwatch类是否返回我期望的结果。

Stopwatch sw = Stopwatch.StartNew();
Thread.Sleep(1);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
在这种情况下,我基本上看到了一个返回值为15ms的结果。我知道DateTime的分辨率大约在那里,但Thread.Sleep(1)不应该让线程睡眠1ms吗?我所在的系统返回Stopwatch.IsHighResolution true,并且运行在.NET 4中。
背景: 这段代码在完整和正确的形式下旨在收集有关Aerospike db获取请求的一些数字。DB不在同一台机器上。当我在查询进行中打印sw.ElapsedMilliseconds时,我看到大多数响应时间都是亚毫秒级别的,这听起来有点可疑,因为我的Java代码等价物大多数情况下都会返回更可信赖的5ms-15ms响应时间。Java代码使用System.nanoTime()的差值。在我的C#代码中,通过Console.WriteLine(sw.ElapsedMilliseconds)打印出的submilli响应意味着0。

我认为对于非常短的时间间隔,线程进入睡眠状态并再次唤醒所需的时间会比实际时间更长。我建议您尝试增加 sleep() 参数,并查看参数和实际休眠时间相等的数字是多少。 - Mehraban
我逐步提高它的速度,直到大约100,但仍然始终看到5到15毫秒的延迟。我知道这不是实时操作系统,但我没有预料到会出现这种情况。 - Rig
我不会说这一定是一个重复的问题,但答案已经在那里了。我应该删除这个问题吗? - Rig
6个回答

36

计时器(Timers)除了Stopwatch之外,都是由时钟中断递增的。在Windows上,默认情况下每秒会有64个时钟中断,即15.625毫秒。因此,如果Thread.Sleep()的参数小于16,则无法得到您想要的延迟时间,您始终会得到至少15.625的间隔。同样地,如果您读取例如Environment.TickCount或DateTime.Now并等待小于16毫秒,则会读取相同的值,并认为已经过去0毫秒。

对于小增量的测量,始终使用Stopwatch,它使用不同的频率源。其分辨率是可变的,取决于主板上的芯片组。但是您可以信赖它比微秒要好。Stopwatch.Frequency可以获取速率。

时钟中断率是可以更改的,您需要调用pinvoke timeBeginPeriod()。这可以使Thread.Sleep(1)准确到单个毫秒。最好不要这样做,因为这样做很耗电。


嗯,总有多媒体定时器可用。 - David Heffernan
对于其他来到这个线程的人,我在Linux/Ubuntu下使用Mono C#进行了测试,发现Thread.Sleep(1)确实可以让线程精确地休眠1毫秒。 - JaredBroad
2
@JaredBroad -- 当然,这个问题是特定于Windows的。(而答案会给你一个提示如何在Windows中“修复”这种行为--但再次强调,它需要大量电力,所以对于大多数情况来说不值得。) - BrainSlugs83
1
这真的帮了我很多。在我的笔记本电脑上,Thread.Sleep 1ms 的简单问题大约需要 1-2ms 的时间。但是,在我的 Azure VM 上执行完全相同的代码却需要 15-16ms 的时间。非常奇怪,但现在我们知道原因了! - Matt Watson

4
我认为Thread.Sleep只是Win32 Sleep API的托管包装器。众所周知,该API函数具有低分辨率。文档如下所述:
此函数会使线程放弃其剩余的时间片,并在基于dwMilliseconds的间隔内变得不可运行。系统时钟以恒定的速率“打勾”。如果dwMilliseconds小于系统时钟的分辨率,则线程可能睡眠时间少于指定的时间长度。如果dwMilliseconds大于一个tick但小于两个tick,则等待时间可以在一到两个tick之间,依此类推。为了增加睡眠间隔的准确性,请调用timeGetDevCaps函数以确定支持的最小计时器分辨率,并调用timeBeginPeriod函数将计时器分辨率设置为最小值。调用timeBeginPeriod时请注意,频繁调用可能会严重影响系统时钟、系统功耗和调度程序。如果您调用timeBeginPeriod,请在应用程序早期调用它一次,并确保在应用程序的最后调用timeEndPeriod函数。
睡眠间隔过去后,线程已准备好运行。如果指定0毫秒,则线程将放弃其剩余的时间片,但仍然准备就绪。请注意,准备就绪的线程不能保证立即运行。因此,线程可能要在睡眠间隔过后的一段时间后才能运行。有关更多信息,请参阅调度优先级。
因此,您可以像建议的那样使用timeBeginPeriodtimeEndPeriod来增加计时器分辨率,但这确实不是推荐的方法。如果您真的需要阻止短时间,则建议您寻找更好的解决方案。例如多媒体定时器。

2

是的,你所得出的结论是完全正确的。

Thread.Sleep不能保证在N毫秒后释放。它只会确保SleepN毫秒后才会释放。

换句话说,Thread.Sleep(1)的意思是至少休眠1毫秒。操作系统不会为特定线程调度特定数量的时间片。

该线程将不会被操作系统调度执行指定的时间量。此方法更改线程的状态以包括 WaitSleepJoin。

来自Msdn


1

Thread.Sleep(n) 的意思是阻塞当前线程,至少要等待 n 毫秒内可能发生的时间片(或线程量子)。不同版本/类型的 Windows 和不同的处理器的时间片长度不同,通常在 15 到 30 毫秒之间。这意味着该线程几乎肯定会阻塞超过 n 毫秒。你的线程恢复的可能性恰好在 n 毫秒后非常小。因此,Thread.Sleep 对于计时是没有意义的。


我实际上并没有使用Thread.Sleep来进行计时,而是用它来验证我的极快响应时间,事实证明它们似乎是准确的。不过,我完全理解你的观点。 - Rig

1

0

Thread.Sleep(1) 会让线程休眠1毫秒,但通常无法达到这种精度。

然而,一旦线程进入睡眠状态,内核需要花费时间将其放回就绪队列,然后在线程再次运行时将其放入运行状态。

所有这些因素都取决于平台、CPU数量、其他进程的数量等。在非实时操作系统中,您无法得到任何保证。


实时操作系统(RT)的设备在这里提供不同的合同吗?这很重要,因为我正在开发一个独立的RT兼容应用程序。 - Rig

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