要約:
- Python の
subprocessモジュールとpsutilライブラリは、この15年間、プロセス終了待機(wait())時にsleepとwaitpidを繰り返す非効率な「Busy-loop ポーリング」方式を使ってきた。 - この方式は、不要な CPU ウェイクアップ、バッテリー消費、プロセス終了検知の遅延(Latency)を招き、多数のプロセスを監視する場合の拡張性(Scalability)にも劣る。
- 最近の更新により、Linux では
pidfd_open()とpoll()、BSD/macOS ではkqueue()を活用した真の「イベント駆動待機(Event-driven waiting)」が実装された。 - Windows はすでに
WaitForSingleObjectを使用しているため変更はないが、POSIX システムでは不要なコンテキストスイッチが取り除かれ、CPU 使用量は「0」に近づく。
詳細要約:
1. 15年間続いた問題: Busy-loop ポーリング
Python 3.3 で subprocess.Popen.wait() に timeout パラメータが追加されて以来、Python 標準ライブラリと広く使われている psutil ライブラリは、プロセス終了を待つために非効率な方式を使ってきました。
従来のロジックは次のように単純ですが非効率でした:
waitpid(WNOHANG)でプロセス状態を確認(Non-blocking)- 終了していなければ短時間
sleep()(指数バックオフを適用) - 1 に戻って繰り返す
# 従来方式(概念コード)
import time, os
def wait_busy(pid, timeout):
delay = 0.0001
while True:
# プロセス終了を確認(polling)
if os.waitpid(pid, os.WNOHANG) == (pid, status):
return status
time.sleep(delay)
delay = min(delay * 2, 0.040) # 最大 40ms まで待機時間を増加
この方式には次の 3 つの致命的な欠点があります。
- CPU Wake-ups: 待機時間をどれだけ伸ばしても、システムは定期的に目を覚まして状態を確認する必要があるため、CPU サイクルを浪費し電力も消費する。
- Latency(遅延): プロセスが実際に終了した時点と、
sleepから復帰してそれを検知する時点の間に、避けられない時間差が生じる。 - Scalability(拡張性): 数百〜数千のプロセスを同時に監視する必要があるサーバー環境では、このオーバーヘッドが急増する。
2. 解決策: POSIX システム向けのイベント駆動待機(Event-driven Waiting)
すべての POSIX システムは、ファイルディスクリプタ(File Descriptor)の状態変化を検知する仕組み(select, poll, epoll, kqueue)を提供しています。最近 Python と psutil は、これをプロセス PID の検知に活用する形へ改善されました。
- Linux: 2019 年に Linux 5.3 カーネルへ導入された
pidfd_open()システムコールを活用する。これはプロセス PID を指すファイルディスクリプタを返し、これをpoll()やepoll()に登録することでプロセス終了イベントを監視できる。(Python 3.9 からosモジュールに追加) - BSD / macOS:
kqueue()システムコールのEVFILT_PROCフィルタを使い、プロセスイベントを効率的に監視する。 - Windows: すでに
WaitForSingleObjectAPI によりイベント駆動待機をサポートしていたため、変更はない。
3. 性能改善と結果
この変更により、wait() 呼び出し時のプロセスは、カーネルの観点では「Interruptible sleep」状態になります。つまり、CPU をまったく消費せずにカーネル空間で静かに待機し、プロセス終了シグナルが発生すると即座に目を覚まします。
/usr/bin/time -v などでベンチマークした結果、従来方式に比べて不要なコンテキストスイッチ(Context Switching)が大幅に減少し、プロセス終了検知速度も即時に改善されました。この更新は psutil ライブラリと CPython コアに反映されており、今後 Python 開発者はコードを修正しなくても性能向上の恩恵を受けられるようになりました。
まだコメントはありません。