イベントリスナーの仕組みを解説する
Javaの実例で紹介
最近、仕事に少し余裕が出てきたのか…コーディング中にふと、イベントリスナーがどのように実装されているのか気になった。
単純に while(true) で動いているなら、星を出力するプログラムをうまく書けなかったときのようにCPUが100%に張り付くはずだ。でも実際にSpringサーバーを立ち上げて確認すると、ほぼ0%を維持している。
シンプルな実装
while (true) {
if (eventOccurred()) {
doSomething();
}
}休みなくイベントが発生したかをチェックし続ける。当然、CPUは100%に達する。
Javaの実際の実装(OSへの委譲)
- Springはサーバー起動時に内部的に
selector.select()を呼び出してイベントを待機する。
実際の実装はこうなっている。(Java nioクラス、Springのログにあるあのnioスレッドで合っている)
// EPollSelectorImpl.java より抜粋
@Override
protected int doSelect(Consumer<SelectionKey> action, long timeout) throws IOException {
int numEntries;
int to = (int) Math.min(timeout, Integer.MAX_VALUE); // タイムアウトを安全に処理
numEntries = EPoll.wait(epfd, pollArrayAddress, NUM_EPOLLEVENTS, to); // LinuxのepollWait()を呼び出し、イベント発生までスレッドはBlocked状態となりCPU使用率0%
return processEvents(numEntries, action); // OSからwakeupイベントが来るとブロックが解除され、numEntriesを返す
}
private int processEvents(int numEntries, Consumer<SelectionKey> action) {
for (int i=0; i<numEntries; i++) {
long event = EPoll.getEvent(pollArrayAddress, i);
int fd = EPoll.getDescriptor(event); // ファイルディスクリプタのオフセットを取得
// ... 省略
processReadyEvents(...) // ハンドラを実行
}
}-
selector.select()を呼び出した瞬間、Linuxはepoll_wait()を呼び出す。 -
OSはそのスレッドを Blocked 状態に切り替える。この状態ではCPU使用率は0%。
-
イベントが発生すると、カーネルがスレッドを起こし、イベントが発生したソケット(チャネル)の一覧を渡す。
ではepoll_wait()の間、イベントをリスニングしながら待つロジックも結局while(true)ではないのか?
epoll_wait()を実装するために、結局while(true)のような待機が必要なのではないか?という考えが浮かんだ。
この例では、NICからパケットが届いたときにepoll_wait()のブロックが解除される必要があり、そこでhardware interruptが使われる。
コンピュータアーキテクチャの講義で確かに聞いた気がするが…聞いただけだったようだ。うろ覚えだ。

-
NICがパケットを受信すると、PICを通じてCPUにIRQ信号を送る。
-
CPUは命令サイクルが終わるたびに Interrupt Request bit を確認する。
割り込みが検出されると、現在のPCとPSWを保存し、ISR(Interrupt Service Routine)のアドレスへジャンプする。 -
ISRがカーネルにイベントを通知し、カーネルが Blocked 状態だったプロセスを Ready 状態に変える。
ISRが終了すると、保存していたPCとPSWを復元し、元のプロセスに戻る。
CPUは基本的に while(true) のように動作しているため、「interrupt を待つ」という表現自体が厳密には誤りだ。(あくまでも interrupt、割り込みなのだから)
まとめ
核心は OSへの委譲 だ。
イベントリスナープログラムがCPUを消費しない理由は、結局「待つロジック」をOSに任せているからだ。
Javaはepoll_wait()でOSに委譲し、OSはhardware interruptでハードウェアに委譲する。
この記事ではネットワークイベントを例に挙げたが、JSのonClick()のような高レベルのイベントリスナーも、深く掘り下げると結局epollを使っている。