💻

イベント駆動なら待機中のCPU使用率をほぼゼロにできる

に公開

恥ずかしながらイベント駆動というものについてほとんど何も知りませんでした。

デーモンなどの常時起動しているシステムは、whileループで sleep(100ms)みたいなことをやるポーリング方式が基本だと思っていました。
この方法が選ばれるケースや、この方法にならざるを得ないケースもあるようですが、イベント駆動方式を使うことができる場合は、イベント待機中のCPU使用率をほぼゼロにできます。

結論

  • イベント駆動方式が使えるならそちらにしたほうが待機中のCPU負荷を抑えられる
    • プログラムを実行する条件(監視したいイベント)をOSのイベント管理機構に登録しておき、イベントの監視はOSの仕組みに任せることで、アプリケーションのプロセス自体がスリープ状態になるため、アプリ待機によるCPUオーバーヘッドはゼロになる
  • ハードに近い制御など、ポーリング方式が適している or 必要なケースもある

ポーリング方式とは

  • forやwhileのループで常に状態を確認し続ける方法です。これは「ビジーウェイト」とも呼ばれます。
  • 動作: ループが絶えず回り続けるため、監視対象に変化がなくてもCPUを常に使用します。
  • CPU消費: 何も仕事をしていない間もCPU時間を消費し続けるため、非効率です。

例:

import time

while True:
    # 絶えずファイルが存在するか確認し続ける
    if a_file_exists():
        do_something()
    time.sleep(0.1) # sleepを入れても短い間隔でCPUが起こされる

ポーリング方式が適している or 必要なケース

以下のようなケースではポーリング方式が使われているようです。

条件 代表例 理由
プロトコル/ハード仕様がプッシュ非対応 USB, FAX, Ping監視 相手が通知できない
IRQ処理オーバーヘッド ≫ データ処理時間 NVMe超低遅延I/O マイクロ秒単位を削りたい
決定論的タイミング・コード簡潔さを最優先 小規模マイコン制御 実装・検証が容易

イベント駆動方式とは

イベントの発生をOSなどに通知してもらい、それを受け取ってから動作を開始する方法です。

  • 動作: イベントを待っている間、プロセスは「スリープ」や「待機(ブロック)」状態になります。この状態では、OSは他のプロセスにCPUを割り当てます。
  • CPU消費: イベントが発生するまで、そのデーモン自体はCPUをほとんど消費しません。
  • 例: ファイルシステムの変更を監視するライブラリ(Pythonのwatchdogなど)は、OSのイベント通知機能(Linuxのinotifyなど)を利用します。

イベント駆動方式だと待機中のCPU使用率は完全に0%になる?

完全に0%にはならないが、ほぼ無視できるレベルになるようです。

geminiより

  • 厳密に言うと、「全くのゼロ」ではなく、プロセスの状態管理や、イベント通知機構そのもののためのごく僅かなリソースは消費される。
  • しかし、ポーリング方式でCPUを常に100%近く(またはsleepを挟んでも定期的に)消費し続けるのに比べれば、イベント駆動方式のCPU使用率は実質的にゼロに近いと言える。

イベント駆動方式の仕組み

イベント駆動方式は、わかりやすく言うと、「用事ができたら教えてください」とOSに依頼して、あとは休んで待つ仕組み(gemini)のようです。
geminiがレストランの順番待ちの例で説明してくれました。


レストランの順番待ちで使われる「呼び出しベル(ページャー)」をイメージすると非常に分かりやすいです。

呼び出しベルで理解するイベント駆動

  • ポーリング方式の場合 👎
    あなたが受付カウンターに張り付いて、「まだですか?」「席は空きましたか?」と数秒おきに店員さん(CPU)に聞き続けるようなものです。あなたは席を案内されるまで、ずっと動き回ってエネルギー(CPUパワー)を使い続けます。

  • イベント駆動方式の場合 👍

    1. 依頼 (Registration) 🛎️
      あなたは受付で「席が空いたら教えてください」と伝え、呼び出しベルを受け取ります
    2. 待機 (Sleep) 😴
      あなたはベルを持って席に座り、スマホを見たりお喋りしたりして自由に過ごします。席が空くのを待つために、エネルギー(CPUパワー)は一切使いません。
    3. 通知と処理 (Notification & Action) 🏃
      席が空くと、店員さんがあなたのベルを鳴らします。あなたはベルが鳴った(イベント発生)ことをきっかけに、初めて受付に向かうという行動を開始します

なぜ待機中のCPU使用がほぼゼロになるのか?

この「呼び出しベル」の仕組みを、コンピュータ内部の**OS(Operating System)**が実現してくれているからです。

  1. OSへの「監視依頼」
    デーモンなどのアプリケーションは、起動するとまずOSに対して「このネットワークポートにデータが来たら」や「このファイルが更新されたら」といった監視したい**イベント(用事)**を登録します。これは、店員さんに呼び出しベルをお願いするのと同じです。

  2. プロセスの「スリープ状態」
    依頼を済ませたアプリケーションを、OSは 「スリープ(待機)」状態 にします。これは、OSが管理する「今すぐCPUを割り当てるべき仕事リスト」から、そのアプリケーションを一時的に外すことを意味します。CPUを動かすOSのスケジューラは、スリープ状態のアプリケーションを完全に無視するため、CPU時間が全く割り当てられなくなります。

  3. OSとハードウェアによる監視
    待っている間、あなたのアプリケーションのコードは一切動いていません。実際の監視は、ネットワークカードなどのハードウェアや、非常に効率的に作られたOS内部の監視専門の仕組みが担当します。彼らがイベントの発生を検知します。

  4. OSによる「叩き起こし」
    イベントが発生すると、ハードウェアやOSの監視機構がOSの中核部分に「用事ができたぞ!」と通知します。それを受けてOSは、そのイベントを待っていたアプリケーションをスリープ状態から「実行可能」状態に戻し(叩き起こし)、「仕事リスト」に復帰させます。

この一連の流れにより、アプリケーションは 「依頼」と「用事が起きた後の処理」の時だけCPUを使い、その間の待ち時間はOSのおかげで完全に休むことができるのです。これが、イベント駆動方式がCPUに優しい理由です。

イベント駆動方式の技術的な説明

これもgeminiの回答ほぼそのままです。


キーポイントは、アプリケーション(ユーザー空間)とOS(カーネル空間)の間の効率的な連携です。ここでは、現代のLinuxで広く使われているepollという仕組みを例に説明します。


技術的な仕組み:epollのステップ

アプリケーションが「ネットワーク接続を待つ」というイベントを処理する流れは、以下のようになります。

1. 準備:ファイルディスクリプタの取得

まず、アプリケーションはOSに「ネットワーク接続を待ち受けるポートを開けてください」とお願いします。OSはこれを受け、そのポートを管理するための番号を発行します。これをファイルディスクリプタ (File Descriptor) と呼びます。この番号が、アプリケーションとOSの間で「どのI/Oか」を識別するための合い言葉になります。

2. 依頼:監視対象の登録

次に、アプリケーションはOSに対して、epollというイベント管理機構を使うことを宣言し、「このファイルディスクリプタでイベント(新しい接続要求など)が起きたら教えてほしい」と登録します。

  • epoll_create: カーネル内に、イベントを監視するための専用スペース(epollインスタンス)を作成します。
  • epoll_ctl: 作成したスペースに、監視してほしいファイルディスクリプタを追加(登録)します。

この時点では、まだアプリケーションは動いています。これはレストランで「これからこのベルで待ちます」と宣言している段階です。

3. 待機:プロセスのスリープ

ここが最も重要なステップです。アプリケーションはepoll_waitという命令(システムコール)をOSに送ります。

この命令は、「登録したファイルディスクリプタのどれかにイベントが発生するまで、このアプリケーションの実行を完全に停止(スリープ)させてください」という意味です。

この命令を受け取ったOS(カーネル)は、アプリケーションをスリープ状態にします。これにより、OSのスケジューラ(どのプログラムにCPUを割り当てるかを決める司令塔)は、このアプリケーションをCPU割り当ての対象から完全に外します。

これが、待機中にCPUを消費しない核心部分です。 アプリケーションのプログラムは文字通り「一時停止」しており、監視のためのループなどは一切回っていません。監視はカーネルが一手に引き受けます。

4. 通知:ハードウェア割り込みと起床

外部からネットワーク接続が来ると、まずPCの ネットワークカード(ハードウェア) がそれを検知します。

  1. ネットワークカードはCPUに対して「データが来たぞ!」という電気信号を送ります。これをハードウェア割り込みと呼びます。
  2. 割り込み信号を受け取ったCPUは、実行中の処理を中断し、OS(カーネル)の割り込み処理プログラムを即座に実行します。
  3. OSはデータがどのファイルディスクリプタに対するものかを確認し、epollの管理スペースに「このファイルディスクリプタでイベント発生」と記録します。
  4. そして、このイベントを待っていたアプリケーションをスリープ状態から復帰させ、「実行可能」な状態に戻します。

5. 再開:処理の実行

アプリケーションが「実行可能」状態になると、スケジューラは再びCPU時間を割り当てます。これにより、停止していたepoll_waitの命令がようやく完了し、アプリケーションに制御が戻ります。

その際、epoll_waitは「どのファイルディスクリプタでイベントが起きたか」の一覧を返します。アプリケーションはその情報をもとに、準備が整ったファイルディスクリプタに対してだけ、読み書きなどの具体的な処理を実行します。


まとめ:ユーザー空間とカーネル空間の連携

この仕組みを図にすると、以下のようになります。

アプリケーション (ユーザー空間) OS (カーネル空間)
1. ポートを開く (socket, bind, listen) → ファイルディスクリプタを発行
2. epollに監視を依頼 (epoll_ctl) → 監視リストにFDを登録
3. epoll_wait() で待機開始 プロセスをスリープ状態にする
(CPU消費ゼロで待機) ハードウェア割り込みを待つ
← ネットワークカードから割り込み発生
イベントを検知し、プロセスを起床させる
4. epoll_wait()から処理が戻る ← イベントが発生したFDリストを渡す
5. 準備ができたFDに対して処理を実行

このように、CPUを消費する可能性がある「監視」という仕事を、アプリケーション自身ではなく、非常に効率的に作られたOSカーネルに丸投げすることで、イベント駆動は待機中のCPU消費をほぼゼロに抑えています。

Discussion