如何睡觉是非

不久前,有一篇关于现代软件性能的糟糕状况的出色文章( 英文 原文,哈布雷翻译本)流传了过去。 本文使我想起了该代码的一种反模式,该模式非常普遍并且通常以某种方式起作用,但是会导致此处和此处的性能损失很小。 好吧,你知道,一个小手,无论如何都不会伸手。 唯一的麻烦是,散布在代码中不同位置的许多“琐事”开始引起诸如“我拥有最新的Intel Core i7和滚动开关”之类的问题。

我说的是睡眠功能的不正确使用(具体情况视编程语言和平台而定)。 那么什么是睡眠? 文档非常简单地回答了这个问题:这是在当前线程执行中暂停指定的毫秒数。 应该注意的是,此函数原型的美学美:

void Sleep(DWORD dwMilliseconds); 

仅一个参数(极其清晰),没有错误代码或异常-它始终有效。 如此精美易懂的功能很少!

阅读此功能的原理时,您会更加尊重该功能。
该函数进入OS线程调度程序,并告诉它:“我和我的线程现在和将来都希望拒绝分配给我们的CPU时间。 给穷人!” 调度程序对此慷慨大方感到有些惊讶,它代表处理器执行了感谢功能,将剩余时间分配给下一个人(并且总是有这些人),并且不包含导致Sleep被假装在指定的毫秒数内传输执行上下文的线程。 美女!

可能出了什么问题? 程序员使用此出色功能的事实并非出于预期目的。

它旨在用于通过某些实际的暂停过程定义的某些外部软件仿真。

正确的示例编号1


我们正在编写“时钟”应用程序,其中您需要每秒更改屏幕上的数字(或箭头的位置)。 此处的“睡眠”功能非常适合:在明确定义的时间段(恰好是一秒钟)内,我们实际上无事可做。 为什么不睡觉呢?

正确的示例编号2


我们正在写一个面包机的月光控制器。 操作算法是由程序之一设置的,看起来像这样:

  1. 进入模式1。
  2. 在其中工作20分钟
  3. 进入模式2。
  4. 在其中工作10分钟
  5. 关掉

这里的一切也很清楚:我们需要时间,这取决于技术流程。 可以使用睡眠。

现在让我们看一下滥用睡眠的例子。

当我需要一些不正确的C ++代码示例时,请转到Notepad ++文本编辑器代码存储库 。 它的代码是如此可怕,以至于肯定存在任何反模式,我什至写过一篇关于它的文章 。 记事本++也没有让我失望! 让我们看看它如何使用睡眠。

错误示例1


在启动时,Notepad ++会检查其进程的另一个实例是否已在运行,如果已运行,则寻找其窗口并向其发送消息,然后关闭自身。 为了检测另一个进程,使用了一种标准方法-全局名为互斥体。 但是编写了以下代码来搜索Windows:

 if ((!isMultiInst) && (!TheFirstOne)) { HWND hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); for (int i = 0 ;!hNotepad_plus && i < 5 ; ++i) { Sleep(100); hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); } if (hNotepad_plus) { ... } ... } 


编写此代码的程序员试图为已经启动的Notepad ++找到一个窗口,甚至设想到这样一种情况,即实际上同时启动了两个进程,因此第一个进程已经创建了全局互斥锁,但尚未创建编辑器窗口。 在这种情况下,第二个过程将等待创建“ 100毫秒内5次”窗口。 结果,我们要么根本就不等待,要么在实际创建窗口的那一刻与从Sleep退出之间失去100 ms。

这是使用Sleep的第一个(也是主要的)反模式。 我们不是在等待事件的发生,而是“在几毫秒内,它将突然变得很幸运。” 我们等待的时间如此之长,以至于一方面我们不会真正让用户烦恼,另一方面,我们有机会等待我们需要的事件。 是的,用户在启动应用程序时可能不会注意到100毫秒的暂停。 但是,如果这种“在推土机上稍等片刻”的做法在项目中被接受并可以接受,那么它可能会以我们出于最琐碎的原因而等待每一步的事实而结束。 这里是100毫秒,另外还有50毫秒,这里是200毫秒-此处我们的程序已经“以某种方式减慢了几秒钟的速度”。

另外,从外观上看长时间运行的代码可能很快就会令人不愉快。 在这种特殊情况下,可以使用SetWindowsHookEx函数,订阅HSHELL_WINDOWCREATED事件-并立即接收窗口创建的通知。 是的,代码变得有点复杂,但实际上是3-4行。 而且我们赢了100毫秒! 最重要的是,在期望不是无条件的情况下,我们不再使用无条件期望的功能。

错误示例2


 HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL); int sleepTime = 1000 / x * y; ::Sleep(sleepTime); 

我真的不明白该代码在Notepad ++中的确切含义以及等待了多长时间,但是我经常看到通用的反模式“启动流并等待”。 人们期望不同的事情:另一个流的开始,从中接收一些数据,结束其工作。 马上有两件事是不好的:

  1. 为了执行多线程操作,必须进行多线程编程。 即 第二个线程的启动假设我们将继续在第一个线程中执行某项操作,这时第二个线程将执行其他工作,第一个线程完成其工作(可能还要再等一会),将获得其结果并以某种方式使用它。 如果我们在启动第二个线程后立即开始“睡眠”-为什么根本需要它?
  2. 必须期望正确。 为了获得适当的期望,有一些行之有效的做法:使用事件,等待函数,回调调用。 如果我们正在等待代码在第二个线程中开始工作,请为此设置一个事件并在第二个线程中发出信号。 如果我们正在等待第二个线程完成工作,则C ++有一个很棒的线程类及其连接方法(或者再次是特定于平台的方法,例如Windows上的WaitForSingleObject和HANDLE)。 等待另一个线程中的工作“几毫秒”只是愚蠢的,因为如果我们没有实时操作系统,那么没有人可以保证第二个线程将启动或到达工作阶段的时间。

错误示例3


在这里,我们看到一个正在等待某些事件的后台线程。

 class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; }; 

我必须承认,这里使用的不是Sleep,而是SleepEx ,它更智能,可以中断等待某些事件(例如异步操作的完成)。 但这根本没有帮助! 事实是while(!M_bTerminate)循环有权无休止地工作,而忽略了从另一个线程调用的RequestTermination()方法,将m_bTerminate变量重置为true。 我在上一篇文章中谈到了这种情况的原因和后果。 为了避免这种情况,您应该使用某些保证在线程之间正常工作的方法:原子,事件或类似方法。

是的,使用常规布尔变量同步线程的问题在形式上不应该归咎于SleepEx,这是另一个类的单独错误。 但是,为什么在这段代码中有可能呢? 因为起初程序员认为“您需要在这里睡觉”,然后才考虑停止这样做的时间和条件。 在正确的情况下,他甚至都不应该先考虑一下。 这种想法应该发生在我的脑海中,“我们应该在这里发生一个事件”,从那一刻起,这种想法就会朝着选择正确的机制在流之间同步数据的方向努力,这将同时排除布尔变量和SleepEx的使用。

错误示例4


在此示例中,我们将查看backupDocument函数,该函数充当“自动保存”,在意外的编辑器崩溃的情况下很有用。 默认情况下,她睡眠7秒钟,然后发出命令以保存更改(如果已保存)。

 DWORD WINAPI Notepad_plus::backupDocument(void * /*param*/) { ... while (isSnapshotMode) { ... ::Sleep(DWORD(timer)); ... ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0); } return TRUE; } 

间隔可以更改,但这不是问题。 任何时间间隔将同时太长和太短。 如果我们每分钟输入一个字母,那么只睡7秒是没有意义的。 如果我们从某个地方复制粘贴10兆字节的文本,那么我们不必再等待7秒钟,它的容量足够大,可以立即启动备份(突然之间,我们从某个地方将其剪下并移到那里,然后编辑器在第二秒后崩溃了)。

即 出于简单的期望,我们在这里替换了缺少的更智能的算法。

错误示例5


记事本++可以“键入文字”-即 通过在字母插入之间暂停来模拟人类文本输入。 它似乎被写成“复活节彩蛋”,但是您可以提出一些可以使用该功能的应用程序( 愚弄Upwork,是的 )。

 int pauseTimeArray[nbPauseTime] = {200,400,600}; const int maxRange = 200; ... int ranNum = getRandomNumber(maxRange); ::Sleep(ranNum + pauseTimeArray[ranNum%nbPauseTime]); ::SendMessage(pCurrentView->getHSelf(), SCI_DELETEBACK, 0, 0); 

这里的麻烦在于,该代码具有某种“普通人”的想法,在每个按键之间会暂停400-800毫秒。 好吧,也许这是“平均”的和正常的。 但是,您知道,如果我使用的程序只是因为它们看起来很漂亮并且适合她而在我的工作中稍作停顿,那根本不意味着我同意她的观点。 我希望能够调整暂停数据的持续时间。 而且,如果在Notepad ++中不是很关键,那么在其他程序中,我有时会遇到“更新数据:经常,通常,很少”这样的设置,其中“经常”对我来说不够频繁,而“很少”对我来说不够很少。 是的,“正常”不正常。 这种功能应使用户能够准确指示他想要等待直到完成所需动作的毫秒数。 使用强制选项输入“ 0”。 此外,在这种情况下,甚至不应将0作为参数传递给Sleep函数,而应仅排除其调用(Sleep(0)实际上不会立即返回,而是将调度程序给定的剩余时隙分配给另一个线程)。

结论


在睡眠的帮助下,当在特定时间段内无条件分配期望时,人们可以并且应该实现期望,并且有逻辑上的解释,为什么会这样:“根据技术过程”,“时间是根据此公式计算的”,“等待了那么久”顾客说。“ 等待某些事件或同步线程不应使用睡眠功能实现。

Source: https://habr.com/ru/post/zh-CN427843/


All Articles