为啥 Thread.Sleep 如此有害

Posted

技术标签:

【中文标题】为啥 Thread.Sleep 如此有害【英文标题】:Why is Thread.Sleep so harmful为什么 Thread.Sleep 如此有害 【发布时间】:2012-02-07 14:37:52 【问题描述】:

我经常看到它提到不应该使用Thread.Sleep();,但我不明白为什么会这样。如果Thread.Sleep(); 会引起麻烦,是否有任何替代解决方案具有相同结果且安全?

例如。

while(true)

    doSomework();
    i++;
    Thread.Sleep(5000);

另一个是:

while (true)

    string[] images = Directory.GetFiles(@"C:\Dir", "*.png");

    foreach (string image in images)
    
        this.Invoke(() => this.Enabled = true);
        pictureBox1.Image = new Bitmap(image);
        Thread.Sleep(1000);
    

【问题讨论】:

博客的摘要可能是“不要滥用 Thread.sleep()”。 我不会说它有害。我宁愿说它就像goto:,即对于您的问题可能有比Sleep更好的解决方案。 goto不完全一样,更像是代码味道而不是设计味道。编译器将gotos 插入您的代码没有任何问题:计算机不会混淆。但是Thread.Sleep 并不完全相同。编译器不会插入该调用,它还会产生其他负面后果。但是,是的,使用它是错误的,因为几乎总是有更好的解决方案,这种普遍看法当然是正确的。 @KevinKostlan 使用计时器或调度框架(如quartz.net)然后... 每个人都在就上述示例为什么不好提供意见,但没有人提供不使用 Thread.Sleep() 的重写版本,它仍然可以实现给定示例的目标。 【参考方案1】:

调用Thread.Sleep的问题是explained quite succinctly here:

Thread.Sleep 有它的用途:在 MTA 线程上测试/调试时模拟冗长的操作。在 .NET 中没有其他理由使用它。

Thread.Sleep(n) 表示至少阻塞当前线程数 n 内可能发生的时间片(或线程量) 毫秒。 不同版本/类型的时间片长度不同 Windows 和不同的处理器,通常范围从 15 到 30 毫秒。这意味着线程几乎可以保证阻塞 超过n 毫秒。您的线程将 在n 毫秒之后重新唤醒几乎是不可能的 不可能的可以。 所以,Thread.Sleep 对计时毫无意义

线程是一种有限的资源,它们大约需要 200,000 个周期 创造和大约 100,000 个循环来摧毁。默认情况下,他们 为其堆栈保留 1 兆字节的虚拟内存并使用 2,000-8,000 每个上下文切换的周期。 这使得任何等待线程 巨大的浪费。

首选解决方案:WaitHandles

最常犯的错误是将Thread.Sleep 与while 构造(demo and answer、nice blog-entry)一起使用

编辑: 我想加强我的回答:

我们有 2 个不同的用例:

    我们正在等待,因为我们知道 我们应该继续的具体时间跨度(使用Thread.SleepSystem.Threading.Timer 或类似名称)

    我们正在等待,因为某些情况会在一段时间内发生变化...... 关键字是/是一段时间!如果条件检查在我们的代码域中,我们 应该使用 WaitHandles - 否则外部组件应该 提供某种钩子……如果没有,它的设计就很糟糕!

我的回答主要涉及用例 2

【讨论】:

考虑到今天的硬件,我不会将 1 MB 内存称为巨大浪费 @Default 嘿,与原作者讨论 :) 而且,它始终取决于您的代码 - 或者更好:因素......主要问题是“线程是有限的资源” - 今天的孩子对某些实现的效率和成本知之甚少,因为“硬件很便宜”......但有时你需要非常选择编码 '这使得任何等待线程成为巨大的浪费' 嗯?如果某些协议规范要求在继续之前暂停一秒钟,那么等待 1 秒钟是什么?某个线程,某处,将不得不等待!线程创建/销毁的开销通常是无关紧要的,因为无论如何都必须出于其他原因引发线程,并且它会在进程的生命周期内运行。当规范说“打开泵后,至少等待十秒钟以使压力稳定后再打开进料阀”时,我有兴趣看到任何避免上下文切换的方法。 @CodyGray - 我又读了一遍帖子。我在我的 cmets 中看不到任何颜色的鱼。 Andreas 退出网络:“Thread.Sleep 有它的用处:在 MTA 线程上测试/调试时模拟冗长的操作。在 .NET 中没有其他理由使用它。我认为有许多应用程序需要调用 sleep() 。如果开发人员(因为有很多人)坚持使用 sleep() 循环作为条件监视器,应该用 events/condvars/semas/whatever 替换,那没有理由断言“没有其他理由使用它” '。 在 30 年的多线程应用程序开发(主要是 C++/Delphi/Windows)中,我从未见过任何可交付代码中需要 sleep(0) 或 sleep(1) 循环。有时,出于调试目的,我会插入这样的代码,但它从未提供给客户。 “如果你写的东西不能完全控制每个线程”——线程的微观管理与开发人员的微观管理一样大。线程管理是操作系统的作用——应该使用它提供的工具。【参考方案2】:

我有一个在这里看不到的用例,我认为这是使用 Thread.Sleep() 的正当理由:

在运行清理作业的控制台应用程序中,我需要对数千个并发用户共享的数据库进行大量相当昂贵的数据库调用。为了不敲击数据库并在几个小时内排除其他数据库,我需要在调用之间暂停,大约 100 毫秒。这与时间无关,只是让其他线程访问数据库。

在可能需要 500 毫秒执行的调用之间花费 2000-8000 个周期进行上下文切换是良性的,线程具有 1 MB 堆栈也是如此,它作为服务器上的单个实例运行。

【讨论】:

是的,在你描述的那种单线程、单一用途的控制台应用程序中使用Thread.Sleep 是完全可以的。【参考方案3】:

我想从编码政治的角度回答这个问题,这可能对任何人都有帮助,也可能没有帮助。但特别是当您处理为朝九晚五的公司程序员设计的工具时,编写文档的人倾向于使用“不应该”和“从不”之类的词来表示“除非您真的知道自己在做什么,否则不要这样做。 “在做什么以及为什么”。

我在 C# 世界中最喜欢的其他几个是它们告诉您“永远不要调用 lock(this)”或“永远不要调用 GC.Collect()”。这两个在许多博客和官方文档中都被强行声明,而 IMO 是完全错误的信息。在某种程度上,这种错误信息达到了它的目的,因为它使初学者在充分研究替代方案之前远离做他们不了解的事情,但同时,它使得通过搜索引擎找到真正的信息变得困难,所有这些似乎指向文章告诉你不要做某事,而没有回答“为什么不?”的问题

从政治上讲,它归结为人们认为“好的设计”或“坏的设计”。官方文档不应该规定我的应用程序的设计。如果确实有技术原因不应该调用 sleep(),那么 IMO 文档应该说明在特定场景下调用它是完全可以的,但可能会提供一些与场景无关或更适合其他场景的替代解决方案情景。

在许多情况下,明确调用“sleep()”是有用的,因为在现实世界中明确定义了截止日期,但是,还有更复杂的系统用于等待和发信号通知线程,在开始之前应该考虑和理解这些系统将 sleep() 放入代码中,并在代码中放入不必要的 sleep() 语句通常被认为是初学者的策略。

【讨论】:

【参考方案4】:

我同意这里的许多观点,但我也认为这取决于。

最近我做了这个代码:

private void animate(FlowLayoutPanel element, int start, int end)

    bool asc = end > start;
    element.Show();
    while (start != end) 
        start += asc ? 1 : -1;
        element.Height = start;
        Thread.Sleep(1);
    
    if (!asc)
    
        element.Hide();
    
    element.Focus();

这是一个简单的动画函数,我在上面使用了Thread.Sleep

我的结论是,如果它有效,请使用它。

【讨论】:

【参考方案5】:

睡眠用于您无法控制的独立程序有时可能会使用常用资源(例如文件)、您的程序在运行时需要访问以及资源在这些其他程序使用您的程序被阻止使用它。在这种情况下,当您在代码中访问资源时,您将对资源的访问置于 try-catch 中(以在您无法访问资源时捕获异常),并将其置于 while 循环中。如果资源是空闲的,sleep 永远不会被调用。但是,如果资源被阻塞,那么您会休眠一段适当的时间,并尝试再次访问该资源(这就是您循环的原因)。但是,请记住,您必须在循环上放置某种限制器,因此它不是潜在的无限循环。您可以将限制条件设置为N次尝试(这是我通常使用的),或者检查系统时钟,添加固定的时间以获得时间限制,如果达到时间限制则退出尝试访问。

【讨论】:

【参考方案6】:

人们要注意的是示例中的 1).spinning 和 2).polling 循环,而不是 Thread.Sleep() 部分。 我认为通常添加 Thread.Sleep() 是为了轻松改进正在旋转或处于轮询循环中的代码,因此它只是与“坏”代码相关联。

此外,人们还会做以下事情:

while(inWait)Thread.Sleep(5000); 

其中变量 inWait 没有以线程安全的方式访问,这也会导致问题。

程序员希望看到的是由Events和Signaling and Locking结构控制的线程,当你这样做时,你就不需要Thread.Sleep(),并且也消除了对线程安全变量访问的担忧.例如,您能否创建一个与 FileSystemWatcher 类关联的事件处理程序并使用事件来触发您的第二个示例而不是循环?

正如 Andreas N. 所说,阅读Threading in C#, by Joe Albahari,它真的很棒。

【讨论】:

那么,除了代码示例中的内容之外,还有什么替代方法? kuhaku - 是的,好问题。显然 Thread.Sleep() 是一个捷径,有时是一个很大的捷径。对于上面的示例,轮询循环“while(inWait)Thread.Sleep(5000);”下面的函数中的其余代码是一个新函数。这个新函数是一个委托(回调函数),您将它传递给设置“inWait”标志的任何东西,而不是更改“inWait”标志,而是调用回调。这是我能找到的最短的例子:myelin.co.nz/notes/callbacks/cs-delegates.html 只是为了确保我明白了,你的意思是用另一个函数包装 Thread.Sleep(),然后在 while 循环中调用它? 您链接的那篇文章仍然相关吗?上次更新是在 10 年前。【参考方案7】:

对于那些在场景 2 中没有看到一个反对使用 Thread.Sleep 的有效论据的人,确实有一个 - 应用程序退出被 while 循环阻止 (场景 1/3 实在是太愚蠢了,不值一提)

许多假装是知情者,大喊 Thread.Sleep 是邪恶的,但没有为我们这些要求不使用它的实际理由的人提到一个正当理由 - 但在这里,感谢 Pete - Thread.Sleep 是邪恶的(可以通过计时器/处理程序轻松避免)

    static void Main(string[] args)
    
        Thread t = new Thread(new ThreadStart(ThreadFunc));
        t.Start();

        Console.WriteLine("Hit any key to exit.");
        Console.ReadLine();

        Console.WriteLine("App exiting");
        return;
    

    static void ThreadFunc()
    
        int i=0;
        try
        
            while (true)
            
                Console.WriteLine(Thread.CurrentThread.ThreadState.ToString() + " " + i);

                Thread.Sleep(1000 * 10);
                i++;
            
        
        finally
        
            Console.WriteLine("Exiting while loop");
        
        return;
    

【讨论】:

-,不,Thread.Sleep 不是原因(新线程和连续的while循环是)!你可以删除Thread.Sleep-line - 瞧:程序也不会退出...... 在这种情况下,使用 Auto/ManualResetEvent 并使用它来代替 Thread.Sleep 是有效的。然后在 Timer 中,设置 auto/manualresetevent 以便循环继续。【参考方案8】:

场景 1 - 等待异步任务完成:我同意 WaitHandle/Auto|ManualResetEvent 应该用于线程正在等待另一个线程上的任务完成的场景。

场景 2 - 计时 while 循环:然而,作为一个 粗略 计时机制(while+Thread.Sleep)对于 99% 不需要确切了解的应用程序来说是完全可以的em> 当被阻塞的线程应该“唤醒*。创建线程需要 200k 周期的论点也是无效的 - 无论如何都需要创建时序循环线程,而 200k 周期只是另一个大数字(告诉我要多少个周期)打开文件/socket/db 调用?)。

所以如果 while+Thread.Sleep 有效,为什么要让事情复杂化呢?只有语法律师才会,实用

【讨论】:

谢天谢地,我们现在有 TaskCompletionSource 可以有效地等待结果。

以上是关于为啥 Thread.Sleep 如此有害的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Thread.sleep 不好用

为啥 Thread.Sleep 不在秒表中注册?

为啥 thread.sleep 在第一次捕获时不停止?

为啥我必须将每个 Thread.sleep() 调用包装在 try/catch 语句中? [复制]

为啥要放弃使用Thread.Sleep

为啥没有“Thread.sleep”的“while(true)”会在 Linux 上导致 100% 的 CPU 使用率,而在 Windows 上却不会?