今日のUnityに関する講義の1つで、ユーザーがボタンを押しているかどうかをフレームごとにチェックして、プレーヤーの位置を更新することについて話し合いました。これは非効率であり、代わりにイベントリスナーを使用する必要があると誰かが言った。
私の質問は、プログラミング言語、またはそれが適用される状況に関係なく、イベントリスナーはどのように機能するのですか?
私の直感は、イベントリスナーが常にイベントが発生したかどうかを確認することを想定しています。つまり、私のシナリオでは、イベントが発生した場合にすべてのフレームを確認することと同じです。
クラスでの議論に基づいて、イベントリスナーは別の方法で動作しているようです。
イベントリスナーはどのように機能しますか?
指定したポーリング例(ボタンがフレームごとにチェックされる)とは異なり、イベントリスナーはボタンが押されたかどうかをチェックしません。代わりに、ボタンが押されたときに呼び出されます。
おそらく「イベントリスナー」という言葉があなたを投げかけています。この用語は、「リスナー」が積極的に何かを聞いていることを示唆していますが、実際には何もしていません。 「リスナー」は、イベントにサブスクライブされる関数またはメソッドにすぎません。イベントが発生すると、リスナーメソッド(「イベントハンドラー」)が呼び出されます。
イベントパターンの利点は、ボタンが実際に押されるまで費用がかからないことです。イベントは「ハードウェア割り込み」と呼ばれるものから発生するため、監視することなくこの方法で処理できます。これは、実行中のコードを一時的にプリエンプトしてイベントを発生させるためです。
一部のUIおよびゲームフレームワークは、「メッセージループ」と呼ばれるものを使用します。これは、後で(通常は短い)期間に実行するイベントをキューに入れますが、そもそもそのイベントをメッセージループに入れるためにハードウェア割り込みが必要です。
Webページ(情報の転送を開始する場所)を際限なく更新するのではなく、電子メールニュースレターのサブスクリプション(更新を受信するために自分で登録し、後で送信者によって送信が開始される)に似たイベントリスナー。
イベントシステムは、サブスクライバーのリストを管理するイベントオブジェクトを使用して実装されます。関心のあるオブジェクト(subscribers、listeners、delegatesなどと呼ばれます)は、自分自身をサブスクライブするメソッドを呼び出すことにより、自分自身をサブスクライブしてイベントを通知できますイベントに追加すると、イベントがリストに追加されます。イベントがfiredの場合はいつでも(用語には以下も含めることができます:called、triggered、invoked、runなど)、各サブスクライバーの適切なメソッドを呼び出してイベントを通知し、何が起こったかを理解するために必要なコンテキスト情報を渡します。
短い、不十分な答えは、アプリケーションがシグナル(イベント)を受信し、ルーチンがその時点でのみ呼び出されることです。
長い説明は少し複雑です。
最新の各アプリケーション† 内部には、通常は半非表示の「イベントループ」があり、イベントを受け取る必要がある正しいコンポーネントにイベントをディスパッチします。たとえば、「クリック」イベントは、現在のマウス座標でサーフェスが表示されているボタンに送信されます。これは最も単純なレベルです。実際には、一部のイベントと一部のコンポーネントがメッセージを直接受信するため、OSはこのディスパッチの多くを実行します。
オペレーティングシステムは、発生時にイベントをディスパッチします。彼らは彼ら自身の運転手から通知されることによって、反応的にそうします。
私は専門家ではありませんが、確かに一部の人はCPU割り込みを使用します。新しいデータが利用可能になると、CPU割り込みを制御するハードウェアがCPUのピンを上げます。 CPUは、受信データを処理するドライバーを起動します。ドライバーは、ディスパッチされるイベント(のキュー)を最終的に生成し、OSに制御を返します。
ご覧のとおり、アプリケーションは実際には常に実行されているわけではありません。これは、イベントが発生したときにOS(sorta)によって起動される一連のプロシージャですが、それ以外の時間は何もしません。
† 注目すべき例外があります。物事が違うかもしれない一度のゲーム
event:起こり得ることの一種。
イベントの発火:イベントの特定の発生。イベントが発生しています。
イベントリスナー:イベントの発生を監視するもの。
イベントハンドラー:イベントリスナーがイベントの発生を検出したときに発生する何か。
event subscriber:イベントハンドラーが呼び出すことになっている応答。
これらの定義は実装に依存しないため、さまざまな方法で実装できます。
これらの用語のいくつかは、ユーザーが同義語を区別する必要がないことが多いため、通常は同義語と誤解されます。
プログラミングロジックイベント。
イベントは、いくつかのメソッドが呼び出されたときです。
イベントの発火は、そのメソッドへの特定の呼び出しです。
イベントリスナーは、イベントハンドラーを呼び出す各イベントの発生時に呼び出されるイベントメソッドのフックです。
イベントハンドラーは、イベントサブスクライバーのコレクションを呼び出します。
イベントサブスクライバー(---)は、イベントの発生に応じてシステムが発生することを意味するアクションを実行します。
外部イベント。
eventは、オブザーバブルから推測できる外部の出来事です。
イベントの発火は、その外部の出来事が発生したと認識できるときです。
イベントリスナーは、通常、オブザーバブルをポーリングすることでイベントの発生を検出し、イベントの発生を検出するとイベントハンドラーを呼び出します。
イベントハンドラーは、イベントサブスクライバーのコレクションを呼び出します。
イベントサブスクライバー(---)は、イベントの発生に応じてシステムが発生することを意味するアクションを実行します。
他の人が指摘した点は、投票はしばしば必要ないということです。これは、イベントがシステムレベルで発生した場合に最も効率的な方法であるイベントハンドラーを自動的に呼び出すイベントファイアリングを使用して、イベントリスナーを実装できるためです。
類推すると、郵便局員がドアをノックして郵便を直接あなたに渡す場合、毎日郵便ボックスをチェックする必要はありません。
ただし、イベントリスナーはポーリングによっても機能します。ポーリングは必ずしも特定の値や他のオブザーバブルをチェックする必要はありません。より複雑になる可能性があります。ただし、全体として、ポーリングのポイントは、何らかのイベントが発生したときに応答できるように推測することです。
類推すると、郵便局員がちょうどその中に郵便物を落とすとき、あなたは毎日郵便箱をチェックしなければなりません。郵便局員にドアをノックするように指示できれば、この投票作業を行う必要はありませんが、多くの場合それは可能ではありません。
多くのプログラミング言語では、キーボードのキーが押されたとき、または特定の時間に呼び出されるだけのイベントを作成できます。これらは外部イベントですが、それらをポーリングする必要はありません。どうして?
これは、オペレーティングシステムがポーリングを行っているためです。たとえば、Windowsはキーボードの状態変化などをチェックし、それを検出するとイベントサブスクライバーを呼び出します。したがって、キーボードプレスイベントをサブスクライブする場合、実際には、それ自体がポーリングするイベントのサブスクライバーであるイベントをサブスクライブしていることになります。
類推して、あなたがアパートの複合施設に住んでいて、郵便局員が郵便を共同の郵便受け領域に降ろしたとします。次に、オペレーティングシステムのようなワーカーは、そのメールをすべての人に確認し、何かを受け取った人のアパートにメールを配信できます。これにより、他のすべての人がメール受信エリアをポーリングする必要がなくなります。
私の直感は、イベントリスナーが常にイベントが発生したかどうかを確認することを想定しています。つまり、私のシナリオでは、イベントが発生した場合にすべてのフレームを確認することと同じです。
クラスでの議論に基づいて、イベントリスナーは別の方法で動作しているようです。
イベントリスナーはどのように機能しますか?
ご想像のとおり、イベントはポーリングを通じて機能します。そして、イベントが何らかの形で外部の出来事に関連している場合、キーボードのキーが押された場合、ポーリングはある時点で発生する必要があります。
イベントが必ずしもポーリングを含む必要がないことも事実です。たとえば、イベントがボタンが押されたときの場合、そのボタンのイベントリスナーは、マウスクリックがボタンにヒットしたと判断したときにGUIフレームワークが呼び出すメソッドです。この場合、マウスクリックを検出するにはポーリングを実行する必要がありましたが、マウスリスナーは、イベントチェーンを通じてプリミティブポーリングメカニズムに接続された、よりパッシブな要素です。
USBデバイスやその他の最新の通信プロトコルには、やり取りのための魅力的なネットワークのようなプロトコルのセットがあり、キーボードやマウスなどのI/Oデバイスがアドホックトポロジ。
興味深いことに、「interrupts」はかなり必須の同期的なものであるため、ad hocネットワーキングトポロジを処理しません。これを修正するために、「interrupts」は、と呼ばれる非同期の高優先度パケットに一般化されましたinterrupt transaction "(USBのコンテキストで)または "message-signaled interrupts " (PCIのコンテキストで)。このプロトコルはUSB仕様で説明されています。
-"図8-31。 " Universal Serial Bus Specification、Revision 2.0 " のBulk/Control/Interrupt OUT Transaction Host State Machine" 222ページ; PDF-page-250(2000-04-27)
要点は、I/Oデバイスと通信コンポーネント(USBハブなど)は基本的にネットワークデバイスのように機能することです。したがって、ポートをポーリングする必要があるなどのメッセージを送信します。これにより、専用ハードウェアラインの必要性が軽減されます。
Windowsなどのオペレーティングシステムは、ポーリングプロセス自体を処理するようです。 USB_ENDPOINT_DESCRIPTOR
のMSDNドキュメント で説明されているように、割り込み/アイソクロナスメッセージについてWindowsがUSBホストコントローラをポーリングする頻度を制御する方法について説明しています。
bInterval
値には、割り込みエンドポイントとアイソクロナスエンドポイントのポーリング間隔が含まれています。他のタイプのエンドポイントの場合、この値は無視する必要があります。この値は、ファームウェアでのデバイスの構成を反映しています。ドライバーは変更できません。ポーリング間隔は、デバイスの速度とホストコントローラーの種類と共に、ドライバーが割り込みまたはアイソクロナス転送を開始する頻度を決定します。
bInterval
の値は、一定の時間を表していません。これは相対値であり、実際のポーリング頻度は、デバイスとUSBホストコントローラーが低速で動作するか、高速で動作するか、高速で動作するかによっても異なります。- "USB_ENDPOINT_DESCRIPTOR構造" 、Microsoft、ハードウェアデベロッパーセンター
DisplayPort のような新しいモニター接続プロトコルも同じように見えます:
マルチストリームトランスポート(MST)
DisplayPort Ver.1.2で追加されたMST(マルチストリームトランスポート)
- Ver.1.1aではSST(Single-Stream Transport)のみが利用可能でした
MSTは単一のコネクタを介して複数のA/Vストリームを転送します
最大63ストリーム。 「レーンごとのストリーム」ではない
- それらのトランスポートされたストリーム間で想定される同期性はありません。 1つのストリームがブランキング期間にある可能性がありますが、他のストリームはそうではありません
コネクション型トランスポート
ストリーム送信の開始前に、AUX CHʼs以上のメッセージトランザクションを介して確立されたストリームソースからターゲットストリームシンクへのパス
残りのストリームに影響を与えないストリームの追加/削除
-滑り台 #14 from "DisplayPortTM Ver.1.2 Overview" (2010-12-06)
この抽象化により、1つの接続から3つのモニターを実行するなど、いくつかの優れた機能が可能になります。
DisplayPortマルチストリームトランスポートでは、3つ以上のデバイスを一緒に接続することもできますが、反対に、「消費者」指向の構成ではありません。単一の出力ポートから複数のディスプレイを同時に駆動します。
- "DisplayPort" 、ウィキペディア
概念的には、これから取り除かなければならない点は、ポーリングメカニズムにより、より一般的なシリアル通信が可能になることです。これは、より一般的な機能が必要な場合に最適です。そのため、ハードウェアとOSは論理システムに対して多くのポーリングを実行します。次に、イベントをサブスクライブするコンシューマーは、独自のポーリング/メッセージパッシングプロトコルを記述する必要なく、下位レベルのシステムによって処理される詳細を楽しむことができます。
結局のところ、キーを押すようなイベントは、ソフトウェアレベルの必須のイベント発生メカニズムに到達する前に、かなり興味深い一連のイベントを通過するように見えます。
プルvsプッシュ
イベントが発生したかどうか、または特定の状態に到達したかどうかを確認するには、主に2つの方法があります。たとえば、重要な配達を待つことを想像してください:
pullアプローチ(ポーリングとも呼ばれます)はより簡単です。特別な機能なしで実装できます。一方で、何も表示せずに余分なチェックを行うリスクがあるため、効率が低下することがよくあります。
一方、Pushアプローチの方が一般に効率的です。コードは、何かする必要がある場合にのみ実行されます。一方、リスナー/オブザーバー/コールバックを登録するためのメカニズムが存在する必要があります1。
1 残念ながら、私の郵便配達員には通常、そのようなメカニズムがありません。
具体的には単一性について-フレームごとにポーリングする以外にプレーヤーの入力をチェックする方法はありません。イベントリスナーを作成するには、ポーリングを実行するために「イベントシステム」や「イベントマネージャー」などのオブジェクトが必要になるため、問題を別のクラスにプッシュするだけです。
確かに、イベントマネージャーを取得すると、フレームごとに入力をポーリングするクラスは1つだけになりますが、これによってパフォーマンスが向上することはありません。これは、このクラスがリスナーを反復処理して呼び出す必要があるため、ゲームによって異なります。設計(リスナーの数やプレーヤーが入力を使用する頻度など)の場合、実際にはコストがかかる可能性があります。
これらすべてとは別に、ゴールデンルールを覚えておいてください-時期尚早な最適化はすべての悪の根源です、これは特に、ビデオゲームで特に当てはまります。このような完全に重要ではありません
OSやフレームワークで、ボタンのプッシュやタイマーのオーバーフロー、メッセージの到着などのイベントを処理するサポートがない限り、ポーリングを使用してこのイベントリスナーパターンを実装する必要があります(どこか下)。
ただし、すぐにパフォーマンスのメリットがないという理由だけで、このデザインパターンから離れないでください。イベント処理の基本的なサポートがあるかどうかに関係なく、これを使用する必要がある理由は次のとおりです。
結論-あなたは幸運にもディスカッションに参加し、投票の代替手段を学びました。この概念を実際に適用する機会を探してください。コードがいかにエレガントであるかを理解できます。
ほとんどの イベントループ は、オペレーティングシステムによって提供されるポーリング多重化プリミティブの上に構築されます。 Linuxでは、そのプリミティブは多くの場合 poll
(2) システムコールです(ただし、古いselect
の場合もあります)。 GUIアプリケーションでは、 display server (eg Xorg 、または Wayland )が( socket(7) =または pipe(7) )をアプリケーションで使用します。 X Window System Protocols and Architecture についてもお読みください。
このようなポーリングプリミティブは効率的です。実際には、カーネルは、一部の入力が完了すると(そして一部の割り込みが処理されると)プロセスを起動します。
具体的には、 widget toolkit ライブラリがディスプレイサーバーと通信してメッセージを待機し、これらのメッセージをウィジェットにディスパッチします。 Qt または [〜#〜] gtk [〜#〜] のようなツールキットライブラリは非常に複雑です(数百万行のソースコード)。キーボードとマウスは、ディスプレイサーバープロセス(このような入力をクライアントアプリケーションに送信されるイベントメッセージに変換する)によってのみ処理されます。
(私は単純化しています。実際、物事ははるかに複雑です)
純粋にポーリングベースのシステムでは、特定のアクションがいつ発生するかを知りたいサブシステムは、そのアクションが発生する可能性があるときはいつでもコードを実行する必要があります。必ずしも一意ではないイベントが発生してから10ミリ秒以内に反応する必要のあるサブシステムが多数ある場合、それらはすべて、イベントが発生したかどうかを少なくとも毎秒100回確認する必要があります。それらのサブシステムが異なるスレッド(またはより悪いことに、プロセス)プロセスにある場合、そのようなすべてのスレッドまたはプロセスを100x /秒で切り替える必要があります。
アプリケーションが監視するものの多くがかなり類似している場合は、多くのことを監視し、それらのいずれかが変更されたかどうかを監視できる、集中型の1つの監視サブシステム(おそらくテーブル駆動)を使用する方が効率的です。たとえば、32個のスイッチがある場合、プラットフォームには32個すべてのスイッチを一度にWordに読み込む機能があり、監視コードがポーリング間でスイッチが変更されたかどうかを確認できます。どのコードがそれらに関心があるかについて心配します。
何かが変化したときに通知が必要なサブシステムが多数ある場合、各サブシステムに独自のイベントをポーリングさせるよりも、目的のイベントが発生したときに専用の監視サブシステムに他のサブシステムに通知する方が効率的です。ただし、イベントに誰も関心がない場合に専用の監視サブシステムを設定すると、リソースの無駄遣いになります。イベントに関心のあるサブシステムが数個しかない場合、関心のあるイベントを監視させるコストは、汎用の専用監視サブシステムをセットアップするコストよりも少ないかもしれませんが、損益分岐点はポイントは、プラットフォームによって大きく異なります。
最も単純な形式では、公開オブジェクトは、何かを公開する必要があるときに実行されるサブスクライバーの指示のリストを保持します。
ある種のsubscribe(x)
メソッドがあり、xはイベントハンドラーがイベントを処理するように設計されている方法に依存します。 subscribe(x)が呼び出されると、xがサブスクライバーの指示/参照のパブリッシャーリストに追加されます。
パブリッシャーには、イベントを処理するためのロジックのすべてまたは一部を含めることができます。イベントが発生したときに、指定されたロジックで通知/変換するために、サブスクライバーへの参照が必要な場合があります。ロジックが含まれておらず、イベントを処理できるサブスクライバーオブジェクト(メソッド/イベントリスナー)が必要な場合があります。両方が混在している可能性が高いです。
イベントが発生すると、パブリッシャーはサブスクライバーの指示/参照のリスト内の各項目に対して反復し、そのロジックを実行します。
イベントハンドラーがどれほど複雑に見えても、そのコアでは、この単純なパターンに従います。
イベントリスナーの例では、イベントハンドラーのsubscribe()メソッドにメソッド/関数/命令/イベントリスナーを提供します。イベントハンドラーは、サブスクライバーコールバックのリストにメソッドを追加します。イベントが発生すると、イベントハンドラーはそのリストを反復処理し、各コールバックを実行します。
実際の例では、Stack Exchangeでニュースレターを購読すると、プロファイルへの参照が購読者のデータベーステーブルに追加されます。ニュースレターを公開するときは、リファレンスを使用してニュースレターのテンプレートに入力し、メールに送信します。この場合、xは単にあなたへの参照であり、パブリッシャーはすべてのサブスクライバーに使用される一連の内部命令を持っています。
イベントリスナーは、メッセージを待つ耳のようなものです。イベントが発生すると、イベントリスナーとして選択されたサブルーチンは、イベント引数を使用して動作します。
常に2つの重要なデータがあります。イベントが発生した瞬間と、このイベントが発生したオブジェクトです。他の議論は何が起こったかについてのより多くのデータです。
イベントリスナーは、発生したことに対する反応を指定します。