在我今天有关Unity的演讲中,我们讨论了通过检查用户是否按下按钮的每一帧来更新播放器位置。有人说这效率低下,我们应该改用事件监听器。
我的问题是,无论使用哪种编程语言或应用哪种情况,事件监听器如何工作?
我的直觉是假定事件侦听器不断检查事件是否已被触发,这意味着,在我的情况下,与检查事件是否被触发的每个帧没有什么不同。
根据课堂讨论,事件监听器似乎以不同的方式工作。
事件监听器如何工作?
在我今天有关Unity的演讲中,我们讨论了通过检查用户是否按下按钮的每一帧来更新播放器位置。有人说这效率低下,我们应该改用事件监听器。
我的问题是,无论使用哪种编程语言或应用哪种情况,事件监听器如何工作?
我的直觉是假定事件侦听器不断检查事件是否已被触发,这意味着,在我的情况下,与检查事件是否被触发的每个帧没有什么不同。
根据课堂讨论,事件监听器似乎以不同的方式工作。
事件监听器如何工作?
Answers:
与您提供的轮询示例(每帧检查一次按钮)不同,事件侦听器根本不检查按钮是否被按下。而是在按下按钮时调用它。
也许“事件侦听器”一词让您大跌眼镜。这个词表明“听众”正在积极地做听的事情,而实际上却什么也没做。“侦听器”仅仅是订阅该事件的功能或方法。事件触发时,将调用侦听器方法(“事件处理程序”)。
事件模式的好处是在实际按下按钮之前没有任何花费。可以通过这种方式处理事件而无需对其进行监视,因为它起源于我们所谓的“硬件中断”,它短暂地抢占了正在运行的代码以触发事件。
某些UI和游戏框架使用一种称为“消息循环”的方法,该事件将事件排队以便在稍后(通常较短)的时间执行,但是您仍然需要硬件中断才能将该事件首先放入消息循环中。
简短而不能令人满意的答案是,应用程序接收到信号(事件),并且仅在该点调用例程。
较长的解释涉及更多的内容。
每个现代应用程序†都有一个内部(通常是半隐藏的)“事件循环”,该事件循环将事件分发到应接收事件的正确组件。例如,“单击”事件被发送到按钮,该按钮的表面在当前鼠标坐标处可见。这是最简单的级别。实际上,由于某些事件和某些组件将直接接收消息,因此OS会执行很多此类调度。
操作系统在事件发生时调度事件。他们通过自己的驾驶员通知来做出反应。
我不是专家,但是可以肯定有些使用CPU中断:当有新数据可用时,它们控制的硬件会在CPU上加一个脚。CPU触发驱动程序,该驱动程序处理传入的数据,该驱动程序最终生成要分派的事件(队列),然后将控制权返回给OS。
因此,如您所见,您的应用程序并非一直都在运行。随着事件的发生,OS(sorta)会触发一堆程序,但在其余时间中什么都不做。
†有一些例外,例如一次游戏可能会产生不同的效果
事件:可能发生的事情。
事件触发:事件的特定发生;事件正在发生。
事件监听器:可以监视事件触发的东西。
事件处理程序:事件侦听器检测到事件触发时发生的事情。
事件订户:事件处理程序应该调用的响应。
这些定义不依赖于实现,因此可以以不同的方式实现。
这些术语中的一些通常被误认为是同义词,因为用户通常不需要区分它们。
编程逻辑事件。
该事件是某个方法被调用时。
一个激发事件是该方法的特定呼叫。
该事件监听器是在被称为上调用该事件处理程序的每个事件触发事件方法的钩。
该事件处理程序调用事件的用户的集合。
该事件的用户(一个或多个)执行任何动作(S)系统是指在应对事件的发生发生。
外部事件。
该事件是外部事件,可以从可观察到的事件中推断出来。
一个事件触发是当外部发生,可以认定为已经发生。
的事件侦听器以某种方式检测到事件的点火,经常通过轮询可观察到的(多个),然后它调用在检测到的事件触发的事件处理程序。
该事件处理程序调用事件的用户的集合。
该事件的用户(一个或多个)执行任何动作(S)系统是指在应对事件的发生发生。
其他人指出的是,通常不需要轮询。这是因为可以通过让事件触发自动调用事件处理程序来实现事件侦听器,这通常是在事件为系统级事件时实现事物的最有效方法。
以此类推,如果邮递员敲门并直接将邮件交给您,则无需每天检查邮箱中的邮件。
但是,事件侦听器也可以通过轮询来工作。轮询不一定需要检查特定值或其他可观察的值。它可能更复杂。但是,总的来说,轮询的重点是推断何时发生某个事件以便可以对其进行响应。
以此类推,当邮政工作人员只是在其中放邮件时,您每天都必须检查您的邮箱。如果您可以指示邮递员敲门,则无需进行此轮询工作,但这通常是不可能的。
在许多编程语言中,您可以编写一个事件,该事件在键盘上的某个按键被按下或在特定时间被调用。尽管这些是外部事件,但您无需轮询它们。为什么?
这是因为操作系统正在为您轮询。例如,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会对逻辑系统进行大量轮询。这样,订阅事件的消费者就可以享受下级系统为他们处理的那些细节,而不必编写自己的轮询/消息传递协议。
最终,诸如按键之类的事件似乎经历了一系列相当有趣的事件,然后才进入软件级的命令式事件触发机制。
具体来说,是关于统一-除了每帧轮询一次之外,没有其他方法可以检查玩家的输入。要创建事件侦听器,您仍然需要“事件系统”或“事件管理器”之类的对象来进行轮询,因此只会将问题推到另一个类。
当然,一旦有了事件管理器,就只有一个类在每一帧轮询输入,但这并没有明显的性能优势,因为现在该类必须遍历侦听器并对其进行调用,具体取决于您的游戏设计(例如,有多少听众以及播放器使用输入的频率)实际上可能会更昂贵。
除此之外,请记住黄金法则- 过早的优化是万恶之源,这在视频游戏中尤其如此,在电子游戏中,渲染每一帧的过程通常要付出如此高昂的代价,以至于像这样的小型脚本优化完全是微不足道的
除非您在OS / Framework中有处理诸如按钮按下或计时器溢出或消息到达之类的事件的支持,否则您将无论如何都必须使用轮询(在其下方)来实现此事件侦听器模式。
但是,不要仅仅因为您没有立即获得性能优势就放弃这种设计模式。这是无论是否有事件处理基础支持都应使用它的原因。
结论-您很幸运地参加了讨论,并了解了轮询的一种替代方法。寻找机会在实践中应用此概念,您将欣赏代码的精美程度。
大多数事件循环都构建在操作系统提供的某些轮询多路复用原语之上。在Linux上,该原语通常是poll
(2)系统调用(但可能是旧的select
)。在GUI应用程序中,显示服务器(例如Xorg或Wayland)正在与您的应用程序通信(通过socket(7)或pipe(7))。还请阅读有关X Window系统协议和体系结构的信息。
这样的轮询原语是有效的。实际上,当完成一些输入(并处理了一些中断)时,内核会唤醒您的进程。
具体而言,您的窗口小部件工具箱库与显示服务器通信,等待消息,并将这些消息分发到窗口小部件。Qt或GTK之类的工具包库非常复杂(数百行源代码)。您的键盘和鼠标仅由显示服务器进程处理(该进程将这些输入转换为发送到客户端应用程序的事件消息)。
(我正在简化;实际上情况要复杂得多)
在基于纯轮询的系统中,可能想知道何时发生某些特定操作的子系统将需要在该操作可能发生的任何时间运行一些代码。如果有许多子系统需要在某个不必要的唯一事件发生后的10ms内做出反应,则它们都必须至少每秒检查100次,是否已经发生了它们的事件。如果这些子系统位于不同的线程(或更糟的是进程)进程中,则需要在每个此类线程或进程中切换100x /秒。
如果应用程序要监视的许多事情非常相似,那么拥有一个集中的监视子系统(也许是表驱动)可能会更有效率,该子系统可以监视许多事物并观察它们是否已更改。例如,如果有32个开关,则平台可能具有一次将所有32个开关读为一个字的功能,从而使监视代码可以检查在轮询之间是否更改了任何开关,如果没有,则不担心哪些代码可能会对它们感兴趣。
如果有许多子系统希望在某些情况发生变化时进行通知,那么拥有专用的监视子系统可以在事件发生时通知其他子系统,这些子系统可能会对它们感兴趣,这可能比让每个子系统轮询自己的事件更有效。但是,如果没有人对任何事件感兴趣,则建立一个专用的监视子系统将纯粹浪费资源。如果只有几个子系统对事件感兴趣,那么让他们监视他们感兴趣的事件的成本可能会比建立通用专用监视子系统的成本要低,但收支平衡不同平台之间的差异会很大。
事件侦听器就像耳朵在等待消息。事件发生时,被选择为事件侦听器的子例程将使用事件参数进行工作。
总有两个重要数据:事件发生的时刻和事件发生的对象。其他争论是关于发生了什么的更多数据。
事件侦听器指定对发生的反应。
最简单的形式是,发布对象维护需要发布内容时要执行的订户指令列表。
它将具有某种subscribe(x)
方法,其中x取决于事件处理程序设计为处理事件的方式。调用subscribe(x)时,x被添加到订户指令/参考的发布者列表中。
发布者可能包含用于处理事件的全部,部分或全部逻辑。事件发生时,它可能仅需要引用订户以使用其指定的逻辑来通知/转换它们。它可能不包含逻辑,并且需要可以处理事件的订阅者对象(方法/事件侦听器)。它最有可能同时包含两者。
当事件发生时,发布者将遍历并执行其订户指令/参考列表中每个项目的逻辑。
无论事件处理程序看起来多么复杂,它的核心都遵循这种简单的模式。
对于事件侦听器示例,您向事件处理程序的subscribe()方法提供了一个方法/函数/指令/事件侦听器。事件处理程序将方法添加到其用户回调列表中。发生事件时,事件处理程序将遍历其列表并执行每个回调。
举一个真实的例子,当您在Stack Exchange上订阅新闻通讯时,对您的个人资料的引用将被添加到订阅者的数据库表中。当该发布新闻时,该参考将用于填充该新闻的模板,并将其发送到您的电子邮件中。在这种情况下,x只是对您的引用,发布者具有一组用于所有订阅者的内部指令。