在我今天有关Unity的演讲中,我们讨论了通过检查用户是否按下按钮的每一帧来更新播放器位置。有人说这效率低下,我们应该改用事件监听器。

我的问题是,无论使用哪种编程语言或应用哪种情况,事件监听器如何工作?

我的直觉是假定事件侦听器不断检查事件是否已被触发,这意味着,在我的情况下,与检查事件是否被触发的每一帧都没有什么不同。 >
基于课堂上的讨论,似乎事件监听器以不同的方式工作。

事件监听器如何工作?

评论

事件监听器根本不检查。当“监听”事件触发时调用它。

是的,但是它如何“监听”,难道不是经常检查吗?

不。 “事件监听器”可能是一个不好的选择。它实际上根本没有“听”到。事件侦听器所做的只是等待事件在触发时被调用,就像其他任何方法一样。在以这种方式被调用之前,它什么都不做。

每次检查按钮是否被按下时,都需要花费时钟周期。仅在实际按下按钮时,事件处理程序(侦听器)才会花费您。

@RobertHarvey-不一定,因为“侦听器”仍需要在较低级别进行持续轮询。您只需将复杂性从您自己的代码层更深地推到硬件中断之类的地方。是的,这通常会更有效,但这不是因为侦听优于轮询,而是因为较低级别的轮询比C#和您与硬件之间的15层抽象层的轮询更有效。

#1 楼

与您提供的轮询示例(每帧检查一次按钮)不同,事件侦听器根本不检查按钮是否被按下。而是在按下按钮时调用它。

也许术语“事件侦听器”正在抛出您。这个词表明“听众”正在积极地做着倾听的事情,而实际上却根本没有做任何事情。 “侦听器”仅仅是订阅该事件的功能或方法。当事件触发时,将调用侦听器方法(“事件处理程序”)。

事件模式的好处是,在实际按下按钮之前,没有任何花费。可以通过这种方式处理事件而无需对其进行监视,因为它起源于我们所谓的“硬件中断”,它短暂地抢占了正在运行的代码来触发该事件。

某些UI和游戏框架使用称为“消息循环”,该事件将事件排队以便在稍后(通常较短)的时间执行,但是您仍然需要硬件中断才能将该事件首先放入消息循环中。

评论


值得一提的是,直到按下按钮才需要付费的原因是按钮是“特殊”的,计算机具有操作系统可以使用的中断和其他特殊功能,这些功能被抽象到用户空间应用程序中。

–whatsisname
18年1月4日在6:10

@whatsisname的内幕很深,但实际上游戏引擎可能无法处理中断,但实际上仍在循环中轮询事件源。只是此轮询是集中的和优化的,因此添加更多的事件侦听器不会增加额外的轮询和复杂性。

– gntskn
18年1月4日在13:01

@PieterGeerkens我猜gntskn意味着作为游戏引擎循环的一部分,有一个步骤可以检查任何未完成的事件。事件将在每个循环以及所有其他每个循环一次的活动中进行处理。不会有单独的循环来检查事件。

–约书亚·泰勒(Joshua Taylor)
18年1月4日在14:29



@Voo:更多的原因是不要在本文中详细介绍这一级别。

–罗伯特·哈维(Robert Harvey)
18年1月4日在20:48

@Voo:我说的是按钮,例如键盘上的物理键和鼠标按钮。

–whatsisname
18年1月4日在22:06

#2 楼

事件侦听器类似于电子邮件通讯订阅(您注册自己以接收更新,其发送随后由发件人启动),而不是无休止地刷新网页(您是在其中启动信息传输的人)。 br />
事件系统是使用事件对象实现的,该对象管理订户列表。感兴趣的对象(称为订户,侦听器,委托等)可以通过调用一种对事件进行订阅的方法来使自己订阅事件的通知,该方法使事件将其添加到其列表中。每当触发事件时(术语也可以包括:调用,触发,调用,运行等),它将在每个订阅者上调用适当的方法,以将事件通知他们,并传递他们需要了解的所有上下文信息发生了什么事。

#3 楼

简短而又不能令人满意的答案是,应用程序接收到信号(事件),并且例程仅在该点被调用。

更长的解释涉及更多内容。

客户端事件从何而来?

每个现代应用程序†都有一个内部的,通常是半隐藏的“事件循环”,该事件循环将事件分派到应接收事件的正确组件。例如,将“单击”事件发送到在当前鼠标坐标处可见其表面的按钮。
这是最简单的级别。实际上,OS会进行很多此类调度,因为某些事件和某些组件将直接接收消息。

应用程序事件从何而来?

操作系统在调度事件时会对其进行调度发生。他们通过自己的驱动程序通知来做出反应。

驱动程序如何生成事件?

我不是专家,但是可以肯定的是,某些使用CPU中断:硬件当有新数据可用时,它们会控制CPU的引脚。 CPU会触发驱动程序,该驱动程序处理传入的数据,该驱动程序最终生成要分派的事件(队列),然后将控制权返回给OS。

因此,如您所见,您的应用程序并不是真正的一直在运行。这是一堆程序,它们会在事件发生时被OS(sorta)触发,但是在其余时间中什么都不做。


†有一些值得注意的异常,例如一次可能会有所不同的游戏


评论


该答案说明了为什么浏览器中不涉及鼠标单击事件的轮询。硬件生成中断=>驱动程序将其解析为OS事件=>浏览器将其解析为DOM事件=> JS引擎为该事件运行侦听器。

– Tibos
18年1月4日在13:09

@Tibos afaict也适用于键盘事件,计时器事件,绘画事件等。

–Sklivvz
18年1月4日在18:23

#4 楼

术语


事件:一种可能发生的事情。
事件触发:特定事件的发生;事件发生。
事件侦听器:监视事件触发的事件。
事件处理程序:事件侦听器检测到事件触发的事件。
事件订阅者:对事件的响应处理程序应该调用。

这些定义不依赖于实现,因此可以用不同的方式实现。

其中一些术语通常被误认为同义词,因为通常

常见方案



编程逻辑事件。


事件是某个方法被调用时的事件。
事件触发是对该方法的特定调用。
事件监听器是事件方法中的一个钩子,该事件方法在每次调用该事件的事件触发时都被调用事件处理程序。
事件处理程序调用事件订阅者的集合。
事件订阅者执行系统要响应事件的发生而执行的任何操作。



外部事件。


事件是可以从可观察对象推断出的外部事件。
事件触发是当可以将外部事件识别为已发生。
事件侦听器通常通过轮询可观察对象以某种方式检测事件触发,然后在检测到事件触发时调用事件处理程序。
事件处理程序调用事件订阅者的集合。
事件订阅者执行系统要响应事件的发生而采取的所有措施。



轮询与将挂钩插入事件的触发机制

其他人指出的是,通常不需要轮询。这是因为可以通过使事件触发自动调用事件处理程序来实现事件侦听器,这通常是在系统级事件发生时实现事物的最有效方法。

类比,您不如果邮递员敲门并直接将邮件交给您,则不必每天检查您的邮箱。

但是,事件侦听器也可以通过轮询来工作。轮询不一定需要检查特定的值或其他可观察的值。它可能更复杂。但是,总的来说,轮询的目的是推断发生了什么事件以便可以对其进行响应。

以此类推,当邮政工作人员刚滴下邮件时,您每天都必须检查您的邮箱邮寄进去。如果可以指示邮政工作人员敲门,则不必进行此轮询工作,但这通常是不可能的。

束缚事件逻辑

在许多编程语言中,您可以编写一个事件,该事件在键盘上的某个按键被按下或在特定时间被调用。尽管这些是外部事件,但您无需轮询它们。为什么?

是因为操作系统正在为您轮询。例如,Windows检查诸如键盘状态更改之类的内容,如果检测到此错误,它将呼叫事件订阅者。因此,当您订阅键盘按键事件时,实际上是在订阅一个本身就是轮询事件的订阅者的事件。

类推,您住的是公寓楼邮递员将邮件投送到公共邮件接收区。然后,类似操作系统的工作人员可以为每个人检查该邮件,然后将邮件传递给收到邮件的人的公寓。这就省去了其他所有人不得不轮询邮件接收区域的麻烦。



我的直觉是假定事件侦听器不断检查事件是否已触发,这意味着,在我的情况下,与检查事件是否已触发的每一帧都没有什么不同。

基于在课堂讨论中,似乎事件监听器的工作方式不同。

事件监听器如何工作?


您怀疑,事件可以通过轮询工作。如果某个事件与外部事件有某种关系,例如按下键盘键,则必须在某个时候进行轮询。

事件不一定需要涉及轮询也同样如此。例如,如果事件是按下按钮时,则该按钮的事件侦听器是GUI框架在确定鼠标单击击中按钮时可以调用的方法。在这种情况下,仍然需要进行轮询才能检测到鼠标单击,但是鼠标侦听器是通过事件链连接到原始轮询机制的更被动的元素。

更新:低级别的硬件轮询

事实证明,USB设备和其他现代通信协议具有一组非常引人入胜的类似于网络的交互协议,从而使I / O设备(包括键盘和鼠标)可以临时参与拓扑。

有趣的是,“中断”是当务之急,同步的事情,因此它们无法处理临时网络拓扑。为了解决这个问题,“中断”已被概括为异步的高优先级数据包,称为“中断事务”(在USB中)或“消息信号中断”(在PCI中)。此协议在USB规范中进行了描述:




-“图8-31。“批量/控制/中断OUT事务主机状态机”通用串行总线规范,修订版2.0“,印刷页面222; PDF-page-250(2000-04-27)


要点似乎是I / O设备和通信组件(如USB集线器)基本上起着网络设备的作用。因此,它们发送消息,这需要轮询其端口等。这减轻了对专用硬件线路的需求。

像Windows这样的操作系统似乎可以自行处理轮询过程。如MSDN文档中有关USB_ENDPOINT_DESCRIPTOR的描述,该文档描述了如何控制Windows多久轮询一次USB主机控制器以获取中断/同步消息:


bInterval值包含以下内容的轮询间隔:中断和同步端点。对于其他类型的端点,应忽略此值。此值反映固件中设备的配置。驱动程序无法对其进行更改。

轮询间隔以及设备的速度和主机控制器的类型共同决定了驱动程序应以何种频率启动中断或同步传输。 bInterval中的值不代表固定的时间量。它是一个相对值,实际的轮询频率还取决于设备和USB主机控制器是在低速,全速还是高速下运行。

-“ USB_ENDPOINT_DESCRIPTOR结构”,硬件开发中心, Microsoft


像DisplayPort这样的较新的监视器连接协议似乎也可以做到这一点:


多流传输(MST)

/>

DisplayPort 1.2版中添加了MST(多流传输)


1.1a版中仅提供了SST(单流传输)



MST通过单个连接器传输多个A / V流。



最多63个流;不是“每个通道的流”


在那些传输的流之间不假定同步性;一个流可能处于消隐期,而其他流则不处于消隐期



面向连接的传输


在流传输开始之前,通过AUX CH通过消息事务建立的从流源到目标流宿的路径
添加/删除流而不影响其余流







-“ DisplayPortTM Ver.1.2概述”中的幻灯片#14(2010-12-06)


这种抽象允许一些巧妙的功能,例如通过一个连接运行3个监视器:


DisplayPort多流传输还允许将三个或更多设备连接在一起,但相反。较少面向消费者的配置:从单个输出端口同时驱动多个显示器。

-“ DisplayPort”,维基百科


从概念上讲,要点与此不同的是,轮询机制允许更通用的串行通信,当您需要更多通用功能时,这真棒。因此,硬件和OS对逻辑系统进行了大量轮询。然后,订阅事件的消费者可以享受下层系统为他们处理的那些细节,而不必编写自己的轮询/消息传递协议。

最终,像按键之类的事件在进入软件级的命令式事件触发机制之前,似乎经历了一系列相当有趣的事件。

评论


关于最后一段,通常不会在低级别进行轮询,操作系统会对外围设备触发的硬件中断做出反应。计算机通常具有许多已连接的设备(鼠标,键盘,磁盘驱动器,网卡),而对所有设备进行轮询将非常低效。

– Barmar
18年1月4日在16:20

但是,您对邮件传递的类比正是我将解释更高级别活动的方式。

– Barmar
18年1月4日在16:21

@Barmar Ya知道,当设备转移到USB连接时,有很多关于它们如何从直接生成中断(如PS / 2键盘那样)变为需要轮询(如USB键盘那样)的讨论,而且一些消息人士声称轮询是由CPU完成的。但是,其他消息来源声称这是在专用控制器上完成的,该控制器将轮询转换为CPU的中断。

–纳特
18年1月4日在16:36

@Barmar您是否会知道哪个是正确的?我可能已经看到更多的消息来源声称CPU会执行轮询,但是使用专用的控制器似乎更有意义。我的意思是,我认为Arduino和其他嵌入式设备往往需要CPU来执行轮询,但是我对x86类型的设备一无所知。

–纳特
18年1月4日在16:37



如果有人可以确认,以便我可以更新此答案,那么我认为现代I / O设备例如那些通过USB连接的设备直接绕过CPU的控制直接写入内存(这既是快速/高效又是安全隐患的原因)。然后,需要使用现代操作系统来轮询内存以检查是否有新消息。

–纳特
18年1月4日在16:59



#5 楼

Pull vs Push

有两种主要策略可以检查事件是否发生或是否达到特定状态。例如,想象等待一个重要的传递:



拉:每隔10分钟,转到您的邮箱并检查是否已传递, />推送:告诉送货员在送货时给您打电话。

拉取方法(也称为轮询)更简单:您无需任何特殊功能就可以实现它。另一方面,它通常效率较低,因为您冒着进行额外检查而又无所作为的风险。

另一方面,push方法通常更有效:您的代码仅在具有做某事。另一方面,它要求您存在一种机制来注册侦听器/观察者/回调1。

1我的邮递员通常缺少这种机制。

#6 楼

具体来说,是关于统一性-除了每帧轮询一次之外,没有其他方法可以检查玩家的输入。要创建事件侦听器,您仍将需要“事件系统”或“事件管理器”之类的对象来进行轮询,因此只会将问题推送到其他类。

一旦获得批准您有一个事件管理器,只有一个类在每一帧轮询输入,但这并没有明显的性能优势,因为现在该类必须遍历侦听器并调用它们,这取决于您的游戏设计(例如在其中,有多少听众以及玩家使用输入的频率实际上可能会更高。

除了所有这些,还请记住黄金法则-过早的优化是万恶之源,在视频游戏中尤其如此,在视频游戏中渲染每一帧的过程通常会花费如此之多,以至于像这样的小型脚本优化是完全不重要的

评论


我不会将中央事件循环视为优化,而是将其写成更具可读性,更易理解的代码,而不是轮询遍及整个代码库。它还允许“合成”事件和不是来自轮询游戏引擎的事件。

– BlackJack
18年1月4日在13:56

@BlackJack我同意,我通常自己用这种方式编写代码,但是OP询问性能。顺便说一句,Unity出人意料地有许多类似这样的可疑代码设计决策,就像几乎到处都具有静态功能一样。

–邓诺
18年1月4日在14:03

#7 楼

除非您在OS / Framework中有处理诸如按钮按下或计时器溢出或消息到达之类的事件的支持,否则您将无论如何都必须使用轮询(在其下方)来实现此事件侦听器模式。

但是,不要仅仅因为您没有立即获得性能优势就放弃这种设计模式。这是无论是否支持事件处理的基础,都应该使用它的原因。

代码看起来更简洁,更孤立(如果正确实现,当然)
基于事件处理程序的代码可以更好地承受更改(因为您通常只修改某些事件处理程序)
如果碰巧在底层事件支持的情况下迁移到平台,则可以重用现有的事件处理程序而摆脱投票代码。

结论-您很幸运地参与了讨论并了解了一种替代投票的方法。寻找机会在实践中应用此概念,您将欣赏代码的优美之处。

#8 楼

大多数事件循环都构建在操作系统提供的某些轮询多路复用基元之上。在Linux上,该原语通常是poll(2)系统调用(但可能是旧的select)。在GUI应用程序中,显示服务器(例如Xorg或Wayland)正在与您的应用程序通信(通过套接字(7)或管道(7))。另请阅读有关X Window系统协议和体系结构的信息。

这种轮询原语非常有效;实际上,当完成一些输入(并且处理了一些中断)时,内核实际上会唤醒您的进程。

具体地,您的小部件工具包库与显示服务器进行通信,等待消息并调度这些消息到您的小部件。 Qt或GTK之类的工具包库非常复杂(数百万行源代码)。您的键盘和鼠标仅由显示服务器进程处理(将这些输入转换为发送到客户端应用程序的事件消息)。

(我正在简化;实际上事情要复杂得多)

#9 楼

在纯粹基于轮询的系统中,可能想知道何时发生某些特定操作的子系统将需要在该操作可能发生的任何时间运行一些代码。如果有许多子系统需要在某个不必要的唯一事件发生后的10ms内做出反应,则它们都必须至少每秒检查100次,是否已经发生了它们的事件。如果这些子系统位于不同的线程(或更糟的是进程)进程中,则需要在每个此类线程或进程中以100x /秒的速度进行切换。

如果应用程序要监视的许多事情都非常相似,拥有一个集中的监视子系统(也许是表驱动)可能更有效,该子系统可以监视许多事物并观察它们是否发生了变化。例如,如果有32个开关,则平台可能具有一次将所有32个开关读为一个字的功能,从而使监视代码可以检查在轮询之间是否更改了任何开关,如果没有,则不

如果有许多子系统希望在发生更改时进行通知,则有一个专用的监视子系统会在发生事件时通知其他子系统,而这些子系统可能会对它们感兴趣。比让每个子系统轮询自己的事件更有效。但是,如果没有人对任何事件感兴趣,则建立一个专用的监视子系统将纯粹浪费资源。如果只有几个子系统对事件感兴趣,那么让他们监视他们感兴趣的事件的成本可能会低于建立通用专用监视子系统的成本,但收支平衡不同平台之间的差异会很大。

#10 楼

事件侦听器就像耳朵在等待消息。事件发生时,被选择为事件侦听器的子例程使用事件参数进行工作。

总是有两个重要数据:事件发生的时刻和事件发生的对象。其他论据是关于发生的事情的更多数据。

事件侦听器指定对发生的事件的反应。

#11 楼

事件侦听器遵循发布/订阅模式(作为订阅者)。

以最简单的形式,发布对象维护需要进行某些发布时要执行的订阅者指令的列表。 />
它将具有某种subscribe(x)方法,其中x取决于事件处理程序如何设计以处理事件。调用subscription(x)时,x被添加到订阅者的指令/引用的发布者列表中。

发布者可以包含用于处理该事件的全部,部分或全部逻辑。事件发生时,它可能只需要引用订户以使用其指定的逻辑来通知/转换它们。它可能不包含逻辑,并且需要可以处理事件的订阅者对象(方法/事件侦听器)。

事件发生时,发布者将遍历并执行其订户指令/参考列表中每个项目的逻辑。

无论事件处理程序看起来多么复杂,其核心都遵循这种简单的模式。

示例

对于事件侦听器示例,您可以提供方法/函数/指令/事件监听器,用于事件处理程序的subscribe()方法。事件处理程序将方法添加到其用户回调列表中。当发生事件时,事件处理程序将遍历其列表并执行每个回调。

对于一个真实的示例,当您在Stack Exchange上订阅时事通讯时,对您的配置文件的引用将添加到订户的数据库表。在发布新闻通讯时,该参考将用于填充新闻通讯的模板,并将其发送到您的电子邮件中。在这种情况下,x只是对您的引用,发布者具有一组用于所有订阅者的内部说明。