为什么当其他应用程序正在运行时,Thread.Sleep 等待时间比请求的时间长?

5

我在C#中使用线程遇到了一个小问题。奇怪的是,当我打开 Chrome 浏览器时,我的线程速度从32ms延迟变成16ms延迟;当我关闭Chrome浏览器时,它又恢复成32ms。我使用Thread.Sleep(1000 / 60)进行延迟。有人能解释一下为什么会出现这种情况,并可能提供可能的解决方案吗?

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading;

 namespace ConsoleApplication2
 {
     class Program
     {
         static bool alive;
         static Thread thread;
         static DateTime last;

         static void Main(string[] args)
         {
             alive = true;
             thread = new Thread(new ThreadStart(Loop));
             thread.Start();

             Console.ReadKey();
         }

         static void Loop()
         {
             last = DateTime.Now;

             while (alive)
             {
                 DateTime current = DateTime.Now;
                 TimeSpan span = current - last;
                 last = current;

                 Console.WriteLine("{0}ms", span.Milliseconds);
                 Thread.Sleep(1000 / 60);
             }
         }
     }
 }

1
@Adam Plocher 当Chrome正在运行时,该线程的速度是平常的两倍,但当我关闭它时,它立即变慢。 - Rob Hendriks
1
@jltrem 当我使用StopWatch时,我遇到了完全相同的问题。 - Rob Hendriks
6
这里没有问题需要解决。Sleep的意思是“至少睡眠这么长时间”,这就是它的作用。当你让一个线程睡眠时,它可能永远不会再次醒来;只要眼前还有更高优先级的线程存在。当你说Sleep时,线程调度程序可以自由决定做任何它喜欢的事情,显然在运行Chrome时,它喜欢做一些不同的事情。为什么呢?谁能说得准呢?调度程序表现如其所述 - 它至少会休眠16毫秒,所以不要担心。 - Eric Lippert
4
如果你想知道实际情况而不是我的猜测,我建议你先阅读《Windows Internals 6th Edition》中八十页长的线程调度算法描述,如果你对此主题感兴趣的话。我提供翻译,不包含解释和其他内容。 - Eric Lippert
4
通常情况下,使用Sleep函数时,如果参数不是零,很可能是个bug。为什么要花费创建线程的代价,然后让它睡眠呢?你不会雇佣一个工人付钱让他睡觉,线程也同理。线程很昂贵,如果不使用它,就把它放回线程池中。 - Eric Lippert
显示剩余8条评论
5个回答

12

这里是确认Matthew正确答案的帖子。在Windows上,Thread.Sleep()的准确性受到时钟中断率的影响。它默认每秒滴答64次,即每15.625毫秒一次。只有当发生这样的中断时,Sleep()才能完成。这里的心理形象是“睡眠”这个词所引起的,处理器实际上是睡着的,而不是执行代码。只有那个时钟中断才会再次唤醒它以恢复执行您的代码。

你选择的1000/60是非常不幸的,因为它要求16毫秒。略高于15.625,所以你总是会至少晚醒2个滴答:2 x 15.625 = 31毫秒。就是你测量到的。

然而,该中断率并非固定不变,可以通过程序进行更改。它通过调用CreateTimerQueueTimer()或传统的timeBeginPeriod()来实现。一般来说,浏览器需要这样做。简单的动画GIF就需要更好的计时器,因为GIF帧时间以10毫秒为单位指定。或者一般的多媒体相关操作都需要它。

程序这样做的一个非常丑陋的副作用是,这种增加的时钟中断率会对整个系统产生影响。就像在您的程序中一样。您的计时器突然变得准确,并且您实际上获得了您要求的睡眠时间,即16毫秒。因此,Chrome将更改速率,可能为每秒1000个刻度。最大支持的速率。当您拥有竞争操作系统时,这对业务有好处

您可以通过选择更接近默认中断率的睡眠时间来避免此问题。如果您要求15,则会获得15.625,Chrome无法对其产生影响。 31是下一个甜点。等等,15.625的整数倍并向下取整。

更新:请注意,此行为最近已更改。从Win10版本2004开始,该效果不再是全局的,因此Chrome无法再影响您的程序。 从Win11开始,具有非活动窗口的应用程序使用默认中断率运行。


4
这可能是因为Chrome(或某个Chrome组件)使用增加了Windows API函数Sleep()分辨率的值调用timeBeginPeriod(),而Sleep()是从Thread.Sleep()中调用的。
更多信息请参见此线程:我能改善Thread.Sleep的分辨率吗? 我几年前在Windows Media Player中注意到了这种行为:我们的一个应用程序的行为会根据Windows Media Player是否正在运行而改变。结果发现WMP正在调用timeBeginPeriod()
然而,一般来说,Thread.Sleep()(以及Windows API Sleep())非常不准确。

3

首先,1000 / 60 = 16 毫秒。

PC时钟的分辨率大约为18-20毫秒,Sleep()DateTime.Now的结果将会四舍五入到该值的倍数。

因此,Thread.Sleep(5)Thread.Sleep(15)将会延迟相同的时间。这可能是20、40甚至60毫秒。你不能得到太多保证,Sleep()的参数只是一个最小值。

另外,占用CPU(即使只有一点点)的另一个进程(Chrome)可能会影响程序的行为。编辑:这与你所看到的相反,因此这里发生了其他的情况。不过,它仍然涉及到时间片的舍入。


这里的问题是Chrome不会占用太多机器资源,它使计时器更加“优秀”。因此很明显占用过多资源并不能说明问题。 - Hans Passant

2
基本上,Thread.Sleep不是非常准确的

Thread.Sleep(1000/60)(计算结果为Thread.Sleep(16))要求线程在休眠16毫秒后返回。但是,该线程可能直到经过更长的时间才能再次执行;例如,32毫秒。

至于为什么Chrome会产生影响,我不知道,但由于Chrome为每个选项卡生成一个新线程,它将对系统的线程行为产生影响。


1
你遇到了DateTime的分辨率问题。应该使用Stopwatch来获得更高的精度。Eric Lippert指出,DateTime只有大约30毫秒的准确度,因此在这种情况下使用它进行测量将无法告诉你任何信息。
测量是问题的一半。你的循环实际上的时间变化是由于Sleep的分辨率(如其他答案所述)造成的。

1
我已经尝试了Stopwatch,它给了我与DateTime相同的输出。 - Rob Hendriks
2
DateTime和线程计时器基本上具有相同的分辨率,这并不奇怪,因此使用其中一个来计时另一个是一个非常糟糕的想法。这就像试图使用未标记的尺子来测量第二个未标记的尺子的长度一样,难以准确地完成。 - Eric Lippert

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