使用 Thread.Sleep 等待的替代方法

Posted

技术标签:

【中文标题】使用 Thread.Sleep 等待的替代方法【英文标题】:Alternatives to using Thread.Sleep for waiting 【发布时间】:2013-06-07 20:59:36 【问题描述】:

首先,我问的问题与C# - Alternative to Thread.Sleep? 或Alternative to Thread.Sleep in C#? 不同。我认为我没有错误地使用它并且需要针对特定​​情况的真正替代品。

在代码分析运行期间,我看到了一个令人惊讶的违规行为:

使用 Thread.Sleep() 是设计缺陷的标志。

此违规行为导致 Peter Richie's article 了解这究竟为何构成不良设计。

我们都知道线程创建是昂贵的,线程阻塞意味着池上的争用。我们也知道每个线程将分配一个兆的内存,所以它应该有很短的生命周期,在 UI 上阻塞是邪恶的,使用睡眠来计时是不可靠的等等等等等等。这让我明白了,如果你真的需要执行一个睡眠,如果不是 Thread.Sleep,你应该使用什么?

Peter 继续提到零睡眠是 Thread.Sleep 的唯一正确使用,它有效地放弃了线程的时间片并允许其他线程进行处理。然后更可怕的是,这只是因为对非托管线程的限制,如果在 CLR 中重新实现,将产生在应用程序中使用 Thread.Sleep 的副作用。事实上,所有关于常见错误用法的要点都是错误用法的好例子。

我在使用 Thread.Sleep 非常成功的生产代码中有以下情况:

等待操作系统释放文件锁定(发现文件锁定问题,等待一秒钟,再试一次,稍后放弃)。 杀死一个进程并等待它不在进程列表中显示(杀死它,检查它没有运行,等待一秒钟,检查它没有运行,强制关闭)。 等待复制缓冲区刷新(检查文件大小,尝试访问它,等待,检查大小是否已更改)。

如果在这种情况下不使用 Thread.Sleep,我还有哪些其他选择?紧密的循环往往会使事情变得更糟,我不认为这会使其使用成为“设计缺陷”,尤其是因为 UI 上没有任何内容,并且仅在后台线程中。软件的本质就是在多线程环境中等待其他事情,外部因素会影响你的代码,有时你需要等待......

【问题讨论】:

你想说什么,Thread.Sleep是不可避免的?然后使用它。但我从不想使用它。 虽然 Peter 的文章有很多优点,但不能原谅它完全错误的事实。 【参考方案1】:

WaitHandle 类型和派生类型提供了一种事件驱动的等待机制,与操作系统相关联。例如,当您有一个Task<T> task 并且您通过访问task.Result 等待结果时,内部实现不会轮询Thread.Sleep 之间的调用。它使用WaitHandle 派生类型来进行等待和同步。

有时基于轮询的方法是必要的,就像您在项目符号列表中给出的一些示例一样,但通常您可以使用事件驱动的方法。并不是Thread.Sleep总是不好 - 只是它经常被滥用

软件的本质就是在多线程环境中等待其他事情,外部因素会影响你的代码,有时你需要等待......

等待没问题。 等待轮询通常不是 (*)。如果有任何方法可以使用事件驱动的等待,您通常应该努力使用它。

我不太清楚您要问的是什么,所以我不会详细说明。如果您发表评论,我可以扩展我的答案。


(*) waiting with polling 不好的理论原因如下:

假设我的代码如下所示:

//START
Begin();
while (!Done())
    Thread.Sleep(D);
//STOP

Begin() 开始一些操作。 Done() 返回true 表示操作已完成。假设这将在大约T 时间之后发生。那么:

线程唤醒并检查条件(调用Done()T/D 次 从STARTSTOP 的持续时间包括预期的D/2,纯粹是因为Thread.Sleep

D 应该选择什么值?随着D 的增加,从STARTSTOP 的预期持续时间线性增加。当您减少D 时,(绑定的)迭代次数会随着1/D 而增加。这两个都很糟糕,找到正确的D 是有问题的。

现在将此与事件驱动的等待进行比较:

//START
Begin();
WaitDone();
//STOP

从理论上讲,只要WaitDone() 以某种方式神奇地等待直到操作完成但不再完成,等待轮询案例中发现的两个问题都有消失了:这个线程等待的时间恰到好处——不多也不少!

重申我开始的观点:在 .NET 中,WaitHandle 类和派生类型有助于这种方法。

【讨论】:

可能是因为投反对票的人一无所知。 +1 以获得出色的答案,尤其是。因为“不是 Thread.Sleep 总是不好的——只是它经常被滥用”。 我倾向于将此作为最佳答案,因为它原谅我在我的特定上下文中使用 Thread.Sleep :) 并且还向所有人强调 WaitHandle 是等待线程的最佳方式结束。我经常使用 ResetEvents,因为我不能很容易地等待信号和超时,但是,在这些少数情况下,我无法摆脱仅使用 Thread.Sleep 的诱惑。以这段代码为例:***.com/questions/16774412/…,我需要减速不杀死操作系统 @BrutalDev 如果您与之交互的事物不支持偶数驱动的异步操作,则执行某些操作的唯一方法是通过轮询。例如,在 Windows 7 和更早版本中,Windows API 根本不允许您等待文件锁定。但在 Windows 8 中,WinRT API 已经过重新设计,以原生支持事件驱动的异步操作。 @MartinJames:在完美的世界中,所有应用程序都会临时复制/写入数据并移动它。通用文件监控可能意味着最终用户可以使用数百个不同的应用程序进行复制,这些应用程序在刷新缓冲区或关闭文件锁方面都是不可预测的。所以成为“东西”是相当准确的。至少我可以放心地忽略那些特定的违规行为,因为我知道自己在做什么,并且我可以在这里改变规则。我通常会使用基于事件的等待,所以这个答案就一般“坏”用法的准确替代而言是正确的恕我直言。 @BrutalDev - 是的。如果一个设计很糟糕,但它是唯一的设计,或者是最不糟糕的设计,那么它就是正确的设计:)【参考方案2】:

嗯,你说了大部分。引用“我们都知道线程创建很昂贵,线程中的阻塞意味着池上的争用”,因此您甚至了解使用线程池的含义。

你也明白阻塞 UI 线程是不好的。

再次查看线程池模型:您有一个线程池,可能每个处理器一个,然后将任务传递给它们。阻止其中一个线程有什么意义?如果它现在没有工作要做,那么它应该简单地继续执行不同的任务。

所以,直接回答你的问题“这让我明白了,如果你真的需要执行睡眠,如果不是 Thread.Sleep,你应该使用什么?”,在一个设计良好的现代程序中,你永远不会需要这样做,您只需为后者安排一个任务。

您应该将池中的线程(就像系统中的处理器一样)视为资源,在不需要时应该将其释放给其他人。

转到您的示例,您对命令式编程范式有点过于投入了。

你不需要等待一个进程消失......我不知道你为什么需要这个,但如果必须等待,那是因为你有工作要在一段时间后执行,你的“延续”功能。您应该为此“继续”设置一个计时器。 文件示例应该有其他机制,如果他们没有......那将是好的操作系统设计。例如,等待缓冲区刷新的唯一安全方法是操作系统原语,例如 fsync。 如果有人写入文件,然后又读取文件,则需要同步机制,而不是定时等待(除非文件是仅追加的,在这种情况下文件本身就是同步机制) .

等待同步机制并不是“坏事”。

【讨论】:

谢谢,好点。详细说明几点: 1. 等待应用程序死机只是为了可靠性,因此检查将确保它确实正常关闭,否则将强制它。 2.这里的问题是操作系统(至少在托管代码中)没有任何东西告诉您文件写入何时完成。查看此示例,了解如何几乎不可避免地减速并等待操作系统:***.com/questions/16774412/… 3. 防病毒软件就是一个会意外锁定的示例。【参考方案3】:

在我的一个项目中,我使用了 2 个线程,但我遇到了 UI 冻结 thnx 到 Thread.Sleep 的问题 ....这解决了我的问题:

    public static void Sleeping(int miliseconds)
    
        var task = Sleep(miliseconds);
        task.Wait();
    

    public static async Task Sleep(int miliseconds)
    
        await Task.Delay(miliseconds);
    

编辑:

正如 Darky711 推荐的那样,更好的方法是:

Task.Delay(1000).Wait();

【讨论】:

Task.Delay(1000).Wait() 是 Thread.Sleep(1000) 的一个很好的替代品。请参阅***.com/a/20084603/3115559 以获得很好的解释。

以上是关于使用 Thread.Sleep 等待的替代方法的主要内容,如果未能解决你的问题,请参考以下文章

java Thread.sleep卡死问题

替代 Thread.Sleep

Java:关于Thread.sleep()

Java - 替代 thread.sleep

java 实现等待时间

Selenium自动化测试脚本中三种等待时间简介