什么时候进行事件轮询比使用观察者模式更好?


41

在某些情况下,对事件进行轮询是否比使用观察者模式更好?我担心使用轮询,并且只有在有人给我一个好的情况下才会开始使用它。我能想到的就是观察者模式比轮询更好。考虑这种情况:

您正在编写汽车模拟器。汽车是一个对象。汽车启动后,您要播放“ vroom vroom”声音片段。

您可以通过两种方式对此进行建模:

轮询:每秒轮询一次汽车对象,以查看其是否打开。打开时,播放声音片段。

观察者模式:将汽车设为观察者模式的主题。当它本身打开时,让它向所有观察者发布“ on”事件。创建一个新的声音对象来聆听汽车。让它实现播放声音片段的“ on”回调。

在这种情况下,我认为观察者模式会获胜。首先,轮询是处理器密集型的。其次,汽车启动时,声音片段不会立即触发。由于轮询周期的原因,最多可能有1秒的间隔。


我几乎认为没有任何情况。观察者模式是实际映射到现实世界和现实生活的模式。因此,我认为没有任何场景会证明不使用它是合理的。
2011年

您是在谈论用户界面事件,还是一般事件?
布莱恩·奥克利

3
您的示例无法消除轮询/观察问题。您只需将其传递到较低级别即可。您的程序仍然需要通过某种机制来确定汽车是否开着。
邓肯

Answers:


55

想象一下,您想获得有关每个发动机循环的通知,例如向驾驶员显示RPM测量值。

观察者模式:引擎针对每个循环向所有观察者发布“引擎循环”事件。创建一个对事件进行计数并更新RPM显示的侦听器。

轮询: RPM显示屏定期向发动机询问发动机循环计数器,并相应地更新RPM显示屏。

在这种情况下,观察者模式可能会松散:引擎周期是一个高频,高优先级的过程,您不想为了更新显示而延迟或停止该过程。您也不想通过引擎周期事件来破坏线程池。


PS: 我也经常在分布式编程中使用轮询模式:

观察者模式:进程A向进程B发送一条消息,该消息显示“每次事件E发生时,向进程A发送一条消息”。

轮询方式:进程A定期向进程B发送一条消息,该消息显示“如果自从上次轮询以来发生了事件E,请立即向我发送一条消息”。

轮询模式会产生更多的网络负载。但是观察者模式也有缺点:

  • 如果进程A崩溃,它将永远不会退订,并且进程B将永久地向其发送通知,除非它能够可靠地检测到远程进程失败(这不是一件容易的事)
  • 如果事件E非常频繁和/或通知携带大量数据,则进程A可能会收到超出其处理能力的事件通知。使用轮询模式,它可以限制轮询。
  • 在观察者模式中,高负载可能会在整个系统中引起“涟漪”。如果使用阻塞式插座,则这些波动会同时发生。

1
好点子。有时为了性能起见,最好也进行轮询。
猎鹰

1
预期的观察者数量也是一个考虑因素。当您期望有大量观察者时,从观察者中更新所有观察者可能会成为性能瓶颈。然后,只需要在某个地方写一个值并让“观察员”在需要时检查该值就容易得多。
Marjan Venema

1
“除非它能够可靠地检测到远程过程故障(这不是一件容易的事)”……除了通过轮询; P。因此,最好的设计是尽可能减少“什么都没有改变”的响应。+1,好答案。
PDR

2
@Jojo:可以,是的,但是随后您将应该属于显示内容的策略放入RPM计数器中。也许用户偶尔会想要一个高精度的RPM显示。
Zan Lynx

2
@JoJo:发布每个第100个事件都是骇客。仅当事件频率始终在正确的范围内,处理事件对引擎的时间不会太长,所有订户都需要可比较的准确性时,它才有效。而且,每RPM需要进行一次模运算,(每秒几千RPM)对CPU而言要比每秒的轮询操作多得多。
nikie 2011年

7

如果轮询过程的运行速度比轮询过程慢得多,则轮询会更好。如果要将事件写入数据库,通常最好轮询所有事件生成器,收集自上次轮询以来发生的所有事件,然后将它们写入单个事务中。如果尝试在事件发生时编写每个事件,那么您可能无法跟上并最终在输入队列填满时遇到问题。在松散耦合的分布式系统中,这也更有意义,在分布式系统中,延迟很高,或者连接建立和拆除成本很高。我发现轮询系统更容易编写和理解,但是在大多数情况下,观察者或事件驱动的消费者似乎提供更好的性能(以我的经验)。


7

当连接可能失败,服务器可能完成工作等操作时,通过网络进行轮询要容易得多。请记住,在一天结束时,TCP套接字需要“轮询”活动消息,否则服务器将假定客户端已经走了。

当您希望保持UI更新时,轮询也很好,但是基础对象的变化非常快,在大多数应用程序中,每秒更新UI的次数不超过几次。

如果服务器能够以非常低的成本响应“无变化”,并且您不必进行太频繁的轮询,并且您没有成千上万的客户进行轮询,那么轮询在现实生活中将非常有效。

但是对于“ 内存中 ”情况,我默认使用观察者模式,因为它通常是最少的工作。


5

轮询有一些缺点,您基本上已经在问题中陈述了它们。

但是,当您要真正将可观察者与任何观察者分离时,它可能是一个更好的解决方案。但是在这种情况下,有时对于观察对象使用可观察的包装可能会更好。

我只会在无法通过对象交互观察到可观察对象的情况下使用轮询,例如在查询数据库(其中没有回调)的情况下,这种情况经常发生。另一个问题可能是多线程,在这种情况下,轮询和处理消息通常比直接调用对象更安全,以避免并发问题。


我不太确定为什么您相信轮询对于多线程来说更安全。在大多数情况下,情况并非如此。当轮询处理程序收到轮询请求时,它必须找出被轮询对象的状态,如果该对象处于更新过程中,则对于轮询处理程序而言是不安全的。在侦听器方案中,只有在推送器处于一致状态时,您才可以收到通知,因此可以避免轮询对象中的大多数同步问题。
Lie Ryan

4

有关轮询何时从通知中接管的一个很好的示例,请查看操作系统网络堆栈。

当网络堆栈启用NAPI时,这对Linux来说意义重大,NAPI是一种网络API,允许驱动程序从中断模式(通知)切换为轮询模式。

使用多个千兆位以太网接口,中断通常会使CPU过载,从而导致系统运行速度超乎预期。通过轮询,网卡将数据包收集在缓冲区中,直到被轮询为止,否则网卡甚至会通过DMA将数据包写入内存。然后,当操作系统准备就绪时,它将轮询卡的所有数据并进行标准的TCP / IP处理。

轮询模式允许CPU以最大处理速率收集以太网数据,而不会产生无用的中断负载。当工作不太忙时,中断模式允许CPU在数据包之间空闲。

秘诀是何时从一种模式切换到另一种模式。每种模式都有其优点,应在适当的地方使用。


2

我喜欢投票!可以吗 是! 可以吗 是! 可以吗 是! 我还在吗 是! 现在呢?是!

正如其他人提到的那样,如果只轮询一次又一次返回相同的未更改状态,则效率可能非常低。这是消耗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时代进行轮询时(即使它不是真正的轮询,只有在有工作要做时才执行工作)。想法是尽可能远离事件处理模型,这意味着非均匀循环,非均匀副作用,非均匀控制流,并且越来越多地朝着对均匀数据和隔离进行统一操作的均匀循环工作。并以使更容易集中于“什么”的方式统一副作用


1
“就像使用条件变量来通知线程唤醒”听起来像是基于事件/观察者的安排,而不是轮询。
乔什·卡斯韦

我发现差异非常微妙,但是通知只是以“需要完成的工作”的形式唤醒线程。例如,观察者模式可能会在调整父控件的大小时,使用动态分派来级联调整大小调用以降低层次结构。事物将立即间接调用其大小调整事件函数。然后他们可能会立即重新粉刷自己。然后,如果我们使用事件队列,则调整父控件的大小可能会将调整大小的事件下推到层次结构中,此时,可能会以延迟的方式调用每个控件的调整大小函数,在这种情况下……

...指向它们然后可能会推送重绘事件,同样,在所有内容调整大小之后,这些事件都将以延迟的方式被调用,并且所有这些事件均来自中央事件处理线程。而且我发现,集中化至少对调试有益,并且能够轻松推断出处理在哪里进行(包括哪个线程)...那么我认为最接近轮询的内容都不是这些解决方案...

例如,LayoutSystem通常会有一个处于休眠状态的,但是当用户调整控件的大小时,它将使用条件变量来唤醒LayoutSystem。然后LayoutSystem调整所有必要控件的大小,然后返回睡眠状态。在此过程中,小部件所驻留的矩形区域被标记为需要更新,这时PaintSystem唤醒并经过这些矩形区域,重新绘制需要在平面顺序循环中重绘的矩形区域。

因此,条件变量本身确实遵循观察者模式来通知线程唤醒,但是除了“要做的工作”以外,我们传递的信息也不多。每个唤醒的系统都致力于在一个非常简单的循环中应用非常相似的任务来处理事物,这与具有非均匀任务的事件队列相反(它可能包含各种事件的折衷处理)。

-4

我为您提供有关观察者模式的概念性思考方式的更多概述。考虑一种类似订阅YouTube频道的方案。订阅该频道的用户数量众多,一旦频道上包含许多视频的任何更新,订户将被通知该特定频道发生了变化。因此,我们得出的结论是,如果通道是SUBJECT,则可以订阅,取消订阅并通知所有在该通道中注册的OBSERVER。


2
这甚至没有尝试解决所问的问题,轮询事件何时比使用观察者模式更好。请参阅如何回答
蚊蚋
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.