我喜欢投票!可以吗 是! 可以吗 是! 可以吗 是! 我还在吗 是! 现在呢?是!
正如其他人提到的那样,如果只轮询一次又一次返回相同的未更改状态,则效率可能非常低。这是消耗CPU周期并显着缩短移动设备电池寿命的秘诀。当然,如果您每次都以不比期望的速度快的速度恢复到新的有意义的状态,那也不是浪费。
但是我喜欢投票的主要原因是它的简单性和可预测性。您可以跟踪代码,轻松查看何时何地发生什么事情以及在哪个线程中发生。如果从理论上讲,如果我们生活在一个轮询可以忽略不计的世界中(尽管事实与现实相去甚远),那么我相信这将简化代码维护工作。这就是轮询和拉动的好处,因为我知道是否可以忽略性能,即使在这种情况下也不应该这样做。
当我在DOS时代开始编程时,我的小游戏是围绕轮询展开的。我从一本我几乎不了解的与键盘中断有关的书中复制了一些汇编代码,并使其存储了键盘状态的缓冲区,这时我的主循环始终在轮询。向上键按下了吗?不。向上键按下了吗?不。现在怎么样?不。现在?是。好的,移动播放器。
尽管浪费极大,但与当今的多任务和事件驱动编程相比,我发现推理起来要容易得多。我确切地知道随时随地发生什么事情,并且更容易保持帧速率稳定和可预测而不会打h。
因此,从那时起,我一直在尝试寻找一种方法,以在不消耗CPU周期的情况下获得其中的一些好处和可预测性,例如使用条件变量通知线程唤醒,从而在该时刻可以拉出新状态,做他们的事情,然后回去睡觉,等待再次被通知。
而且我发现事件队列至少比观察者模式更容易使用,即使它们仍然无法轻松预测您将要去的地方或将要发生的事情。他们至少将事件处理控制流集中到系统中的几个关键区域,并始终在同一线程中处理这些事件,而不是从一个功能弹跳到中央事件处理线程之外突然完全出乎意外的地方。因此,二分法并不一定总是介于观察者和轮询之间。事件队列在那儿是中间立场。
但是,是的,以某种方式,我发现对系统所做的事情进行推理的过程要容易得多,这些事情类似于我以前投票时曾经拥有的那种可预测的控制流,而只是抵消了工作在没有状态变化发生的时间。因此,如果您能够以不会像条件变量那样不必要地消耗CPU周期的方式进行操作,就会有好处。
同类环
好吧,我对此发表了很好的评论Josh Caswell
,指出我的回答有些愚蠢:
“就像使用条件变量来通知线程唤醒”听起来像基于事件/观察者的安排,而不是轮询
从技术上讲,条件变量本身正在应用观察者模式来唤醒/通知线程,因此将其称为“轮询”可能会令人难以置信的误导。但是我发现它提供了与从DOS时代进行轮询类似的好处(就控制流和可预测性而言)。我会尽力解释。
那时,我发现吸引人的地方是您可以查看一段代码或对其进行跟踪,然后说:“好的,这整个部分专用于处理键盘事件。在此部分代码中,什么都不会发生我确切地知道在此之前会发生什么,而且我确切地知道在此之后将会发生什么(例如物理和渲染)。” 键盘状态的轮询使您可以集中控制流,以处理应对此外部事件进行的处理。我们没有立即响应此外部事件。我们在方便时对此做出了回应。
当我们使用基于观察者模式的基于推送的系统时,我们常常会失去这些好处。控件可能会调整大小,从而触发调整大小事件。当我们跟踪它时,我们发现我们处于异国情调的控件内,该控件在调整大小时会执行很多自定义操作,从而触发更多事件。最终,我们对进入系统中的所有这些级联事件感到完全惊讶。此外,我们可能会发现,所有这些甚至都不会在任何给定线程中持续发生,因为线程A可能会在此处调整控件的大小,而线程B稍后也会调整控件的大小。因此,鉴于预测一切发生在何处以及将发生什么事有多么困难,所以我总是很难推理。
对于我来说,事件队列要简单一些,因为它简化了所有这些事情的发生位置,至少在线程级别。但是,可能会发生许多不同的事情。一个事件队列可以包含要处理的各种事件,每个事件仍然可以使我们惊讶于发生了什么级联的事件,处理事件的顺序以及我们最终如何在代码库中的所有位置弹跳。
我正在考虑“最接近”轮询的内容不会使用事件队列,而是会延迟非常同类的处理。PaintSystem
可能会通过条件变量提醒A ,需要进行绘制工作才能重新绘制窗口的某些网格单元,此时,它会通过单元格进行简单的顺序循环,并以适当的z顺序重新绘制其中的所有内容。这里可能有一个调用间接/动态调度的级别,以触发驻留在需要重绘单元格中的每个小部件中的绘制事件,仅此而已-只是一层间接调用。条件变量使用观察者模式来提醒PaintSystem
它有工作要做,但它没有指定更多的东西,并且PaintSystem
在这一点上致力于完成一个统一的,非常相似的任务。当我们调试和跟踪PaintSystem's
代码时,我们知道除了绘画之外不会发生其他事情。
因此,主要是要使系统下降到让这些事情对数据执行同质循环的位置,对它们应用非常单一的责任,而不是对不同类型的数据执行各种职责的非同质循环,就像事件队列处理可能会得到的那样。
我们针对此类事情:
when there's work to do:
for each thing:
apply a very specific and uniform operation to the thing
相对于:
when one specific event happens:
do something with relevant thing
in relevant thing's event:
do some more things
in thing1's triggered by thing's event:
do some more things
in thing2's event triggerd by thing's event:
do some more things:
in thing3's event triggered by thing2's event:
do some more things
in thing4's event triggered by thing1's event:
cause a side effect which shouldn't be happening
in this order or from this thread.
依此类推。而且每个任务不必是一个线程。一个线程可能会为GUI控件应用布局(调整大小/重新定位)逻辑并重新绘制它们,但可能无法处理键盘或鼠标单击。因此,您可以将其视为只是改善事件队列的同质性。但是我们也不必使用事件队列,也不需要交错调整大小和绘画功能。我们可以这样做:
in thread dedicated to layout and painting:
when there's work to do:
for each widget that needs resizing/reposition:
resize/reposition thing to target size/position
mark appropriate grid cells as needing repainting
for each grid cell that needs repainting:
repaint cell
go back to sleep
因此,上述方法仅使用条件变量在工作要做时通知线程,但不会交织不同类型的事件(在一个循环中调整大小,在另一个循环中绘制,而不是两者混合),并且它不会无需交流确切需要完成的工作(线程在醒来时通过查看ECS的系统范围状态来“发现”该信息)。这样,它执行的每个循环本质上都是非常同质的,因此很容易推断出一切发生的顺序。
我不确定该怎么称呼这种方法。我还没有看到其他GUI引擎执行此操作,这是我自己的一种特殊方法。但是在我尝试使用观察者或事件队列来实现多线程GUI框架之前,我在调试它时遇到了巨大的困难,并且遇到了一些晦涩的比赛条件和死锁,这些问题和死锁我不够聪明,无法以某种方式使我充满信心关于解决方案的信息(有些人可能可以做到这一点,但我不够聪明)。我的第一个迭代设计只是直接通过信号调用一个插槽,然后一些插槽会生成其他线程来进行异步工作,这是最难于推理的,而且我在竞争条件和死锁上陷入了困境。第二次迭代使用了事件队列,因此推理起来要容易一些,但我的大脑还不容易做到这一点,而又还没有陷入模糊的僵局和种族状况。第三次也是最后一次迭代使用上述方法,最后使我可以创建一个多线程GUI框架,即使像我这样的笨拙的简单人也可以正确实现。
然后,这种最终的多线程GUI设计类型使我可以提出其他一些更容易推理的东西,并避免了我往往会犯的那些类型的错误,这也是我发现在其中进行推理非常容易的原因之一。至少是由于这些同质循环以及它们有点类似于控制流,类似于我在DOS时代进行轮询时(即使它不是真正的轮询,只有在有工作要做时才执行工作)。想法是尽可能远离事件处理模型,这意味着非均匀循环,非均匀副作用,非均匀控制流,并且越来越多地朝着对均匀数据和隔离进行统一操作的均匀循环工作。并以使更容易集中于“什么”的方式统一副作用