イベントリスナーの仕組みを解説する

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(...) // ハンドラを実行
    }
}
  1. selector.select() を呼び出した瞬間、Linuxは epoll_wait() を呼び出す。

  2. OSはそのスレッドを Blocked 状態に切り替える。この状態ではCPU使用率は0%。

  3. イベントが発生すると、カーネルがスレッドを起こし、イベントが発生したソケット(チャネル)の一覧を渡す。

ではepoll_wait()の間、イベントをリスニングしながら待つロジックも結局while(true)ではないのか?

epoll_wait()を実装するために、結局while(true)のような待機が必要なのではないか?という考えが浮かんだ。

この例では、NICからパケットが届いたときにepoll_wait()のブロックが解除される必要があり、そこでhardware interruptが使われる。

コンピュータアーキテクチャの講義で確かに聞いた気がするが…聞いただけだったようだ。うろ覚えだ。

structure

  1. NICがパケットを受信すると、PICを通じてCPUにIRQ信号を送る。

  2. CPUは命令サイクルが終わるたびに Interrupt Request bit を確認する。
    割り込みが検出されると、現在のPCとPSWを保存し、ISR(Interrupt Service Routine)のアドレスへジャンプする。

  3. ISRがカーネルにイベントを通知し、カーネルが Blocked 状態だったプロセスを Ready 状態に変える。
    ISRが終了すると、保存していたPCとPSWを復元し、元のプロセスに戻る。

CPUは基本的に while(true) のように動作しているため、「interrupt を待つ」という表現自体が厳密には誤りだ。(あくまでも interrupt、割り込みなのだから)

まとめ

核心は OSへの委譲 だ。

イベントリスナープログラムがCPUを消費しない理由は、結局「待つロジック」をOSに任せているからだ。
Javaはepoll_wait()でOSに委譲し、OSはhardware interruptでハードウェアに委譲する。

この記事ではネットワークイベントを例に挙げたが、JSのonClick()のような高レベルのイベントリスナーも、深く掘り下げると結局epollを使っている。