为什么Thread.Sleep如此有害?

156

我经常看到人们提到不应该使用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);
    }
}

3
博客的摘要可能是“不要误用Thread.sleep()”。 - Martin James
6
我不会说它是有害的,我更倾向于说它像“goto:”,也就是说,解决问题可能有比“Sleep”更好的方案。 - default
10
这句话的意思是:“Thread.Sleepgoto 并不完全相同,goto 更像是一种代码异味而不是设计异味。编译器在你的代码中插入 goto 并没有错,计算机不会困惑。但是 Thread.Sleep 不完全相同;编译器不会插入这个调用,并且它还有其他负面影响。但是,使用它是错误的这一总体观点几乎始终是正确的,因为几乎总有更好的解决方案。” - Cody Gray
1
@KevinKostlan 使用一个计时器或调度框架(如quartz.net),然后... - user57508
41
大家都在提供关于为什么上述示例不好的意见,但没有人提供一种不使用Thread.Sleep()的重写版本,仍然可以实现给定示例的目标。 - StingyJack
显示剩余6条评论
9个回答

187
调用Thread.Sleep的问题在这里已经相当简洁地解释了

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

Thread.Sleep(n) 的意思是至少阻塞当前线程n毫秒内可以发生的时间片(或线程量子)的数量。 不同版本/类型的Windows和不同的处理器的时间片长度不同,通常从15到30毫秒不等。这意味着线程几乎肯定会被阻塞超过n毫秒。您的线程恢复后正好经过n毫秒的可能性几乎与不可能一样。因此,Thread.Sleep 对于计时来说是无意义的。

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

首选解决方案:等待句柄

最常见的错误是在 while 结构中使用 Thread.Sleep(请参考 演示和答案优秀博客文章

编辑:
我想进一步完善我的回答:

我们有2个不同的使用情况:
1. 我们在等待,因为我们知道一个特定的时间范围,应该继续执行(使用Thread.Sleep、System.Threading.Timer或类似方法)。
2. 我们在等待,因为某些条件改变了一段时间……关键字是“一段时间”!如果条件检查在我们的代码领域内,我们应该使用WaitHandles——否则外部组件应该提供某种钩子……如果它没有,那么它的设计就有问题!
我的答案主要涵盖了使用情况2。

35
考虑到现今的硬件水平,我不认为1 MB 的内存是一种巨大的浪费。 - default
17
@默认 嘿,和原作者讨论这个问题 :) ,而且它总是取决于你的代码——或者更好地说:那个因素……主要问题是“线程是有限资源”——现在的孩子们对某些实现的效率和成本了解不多,因为“硬件很便宜”……但有时你需要编写非常优化的代码。 - user57508
12
“这会使得任何等待的线程都变成了巨大的浪费”,如果某个协议规范要求在继续之前暂停一秒钟,那么将会有什么等待一秒钟?在某个地方,一些线程将不得不等待!线程创建/销毁的开销通常不重要,因为线程必须为其他原因而提高,并且它在进程的生命周期内运行。我很想看到有什么方法可以避免上下文切换,当一个规范说“打开泵后,在打开进料阀之前等待至少十秒钟以使压力稳定”。 - Martin James
11
@CodyGray - 我重新阅读了帖子,我在我的评论中没有看到任何颜色的鱼。Andreas从网页中摘出了一段话:'Thread.Sleep有其用途:在MTA线程上进行测试/调试时模拟长时间操作。在.NET中没有其他理由使用它'。我认为有很多应用程序需要调用sleep()函数。如果有许多开发人员(因为有很多人)坚持使用sleep()循环作为条件监视器,这应该被替换为事件/条件变量/信号量等,这并不能证明'没有其他理由使用它'是正确的。 - Martin James
11
我有30年多线程应用程序开发经验,主要使用C++/Delphi/Windows。在任何可交付的代码中,我从未见过需要使用sleep(0)或sleep(1)循环的情况。有时为了调试目的,我会插入这样的代码,但它从未被发送给客户端。“如果你正在编写的内容不能完全控制每个线程”,微观管理线程像微观管理开发人员一样是一个大错误。线程管理是操作系统存在的原因——应该使用其提供的工具。 - Martin James
显示剩余22条评论

38

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

场景2 - 计时 while 循环:然而,对于 99% 的应用程序而言,粗略的计时机制(while+Thread.Sleep)是完全可以接受的,因为它们不需要知道阻塞的线程何时“唤醒”。说创建线程需要 200k 循环次数也是无效的论点 - 计时循环线程总得被创建,并且 200k 循环次数只是另一个大数字(告诉我打开文件/套接字/数据库需要多少个循环?)。

所以,如果 while+Thread.Sleep 能够运行,为什么要把事情复杂化呢?只有语法专家才会介意,实践才是最重要的!


感谢现在我们有TaskCompletionSource可以高效地等待结果。 - Austin Salgat
Thread.Sleep根本就不按照规定的方式工作,所以它永远都不应该被使用。Sleep(16)的平均时间是30毫秒 - 这完全是错误的。这个函数需要从API中删除。 - Gavin Williams

19

从编码与政治的角度来回答这个问题,可能对某些人有用,也可能没有。但特别是当你处理面向9-5公司程序员的工具时,撰写文档的人经常使用"不应该"和"永远不要"这样的词语来表示"除非你真正知道在做什么以及为什么,否则不要这样做"。

C#领域中我最喜欢的几个例子包括告诉你"永远不要调用lock(this)"或者"永远不要调用GC.Collect()"。这两个在许多博客和官方文档中都被强烈声明,但在我看来完全是错误的信息。某种程度上,这种错误信息起到了作用,因为它防止初学者在没有充分研究替代方案之前做他们不理解的事情,但同时,它使得很难通过搜索引擎找到真正的信息,而所有的文章似乎都在告诉你不要做某件事,却没有回答"为什么不能?"的问题。

在政治上,关键在于人们认为什么是"好的设计"或"坏的设计"。官方文档不应该规定我的应用程序的设计。如果确实有技术上的原因不应该调用sleep(),那么在我看来,文档应该说明在特定情况下调用它是完全可以的,但可能提供一些场景独立或更适合其他场景的替代方案。

显然,在许多情况下,调用"sleep()"是有用的,特别是在与现实世界时间相关的严格期限中。然而,在你把sleep()放入你的代码之前,应该考虑并了解更复杂的等待和线程信号系统,并且在代码中抛入不必要的sleep()语句通常被认为是初学者的做法。


6

Sleep被用于独立的程序(例如文件)可能被其他程序占用,从而导致您的程序无法访问该资源。在这种情况下,您可以在代码中使用try-catch来捕获异常并将其放入while循环中。如果资源可用,则不会调用sleep。但是,如果资源被阻塞,则需要适当地休眠一段时间,并尝试再次访问资源(这就是为什么需要循环)。但是,请注意,必须对循环设置某种限制,以避免潜在的无限循环。您可以将限制条件设置为N个尝试次数(这是我通常使用的方法),或者检查系统时钟,添加固定的时间限制,如果达到时间限制则停止访问。


6
人们警告的是你的示例中的1).自旋和2).轮询循环,而不是Thread.Sleep()部分。我认为Thread.Sleep()通常被添加到正在自旋或轮询循环中的代码中,以便轻松改进代码,因此它只与“糟糕”的代码相关联。

此外,人们会做一些事情:

while(inWait)Thread.Sleep(5000); 

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

程序员希望看到由事件、信号和锁定构造控制的线程,这样你就不需要Thread.Sleep(),并且关于线程安全变量访问的担忧也被消除了。例如,您可以创建与FileSystemWatcher类相关联的事件处理程序,并使用事件触发您的第二个示例,而不是循环。

正如Andreas N.所提到的,阅读Threading in C#,by Joe Albahari,它真的非常好。


那么除了你代码示例中的方法,还有什么替代方案呢? - shinzou
kuhaku - 是的,好问题。显然Thread.Sleep()是一种快捷方式,有时候是一个很大的快捷方式。对于上面的例子,在轮询循环“while(inWait)Thread.Sleep(5000);”下面的函数中,其余的代码是一个新的函数。这个新函数是一个委托(回调函数),你将它传递给设置“inWait”标志的任何东西,并且在不改变“inWait”标志的情况下调用回调。这是我能找到的最短的例子:http://www.myelin.co.nz/notes/callbacks/cs-delegates.html - mike
为了确保我理解正确,您的意思是将Thread.Sleep()包装在另一个函数中,并在while循环中调用它? - shinzou
你提供的那篇文章还有参考价值吗?它最后更新于10年前。 - David Klempfner
@Mike Thread.Sleep的工作方式与规定不符,这是个大问题。你要求它暂停线程X毫秒,但直到过去2*X毫秒后才会看到线程恢复。这简直太糟糕了,无法使用。这与其他线程访问inWait没有任何关系,那只是一个相当具体的警告。 - Gavin Williams

5

我有一个使用情况,没有在这里得到很好的覆盖,我认为这是使用Thread.Sleep()的一个有效原因:

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

在调用可能需要500毫秒执行的情况下,在调用之间花费2000-8000个周期进行上下文切换是无害的,同时线程在服务器上以单个实例运行,具有1 MB的堆栈。


1
是的,在像您描述的这样的单线程、单用途控制台应用程序中使用 Thread.Sleep 是完全可以的。 - Theodor Zoulias

0

我认为使用计时器是一个不错的选择。如果你想让某个动作每隔x毫秒执行一次,如果你使用Thread.sleep(x),那么你的动作将会每隔x+y秒执行一次,其中y是你的动作执行所需的时间。


-8

对于那些没有看到关于在SCENARIO 2中使用Thread.Sleep的一个有效反对意见的人来说,确实存在这样一个问题 - 当while循环阻塞应用程序退出时。

许多自称知情人士,大声疾呼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;
    }

9
不,Thread.Sleep不是这个问题的原因(新线程和持续的while循环才是)。你只需要删除Thread.Sleep这一行代码即可,程序也不会退出... - user57508
在这种情况下,使用Auto/ManualResetEvent代替Thread.Sleep是有效的。然后在计时器中设置自动/手动重置事件,以便循环可以继续。 - Rafael Diego Nicoletti

-8

我同意这里很多人的观点,但我认为这取决于情况。

最近我写了这段代码:

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

我的结论是,如果它能完成工作,就使用它。


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