プロンプトとスコープ
Python 3.14にはInterpreterPoolExecutorが追加されました。各ワーカーインタプリタは独自のGILを持ち、Pythonコードを並列に実行できますが、呼び出しと結果の受け渡しは分離とシリアライズの境界を越える必要があります。この質問は並行処理のトレードオフを検証するものであり、codingに属します。すべてのワークロードがプロセスやネイティブ拡張よりも高速になると保証するものではありません。
面接官が評価するポイント
優れた回答では、インタプリタの分離、pickle化可能性、モジュールとグローバル状態、イニシャライザの挙動、キャンセル処理、メモリコストについて説明します。CPUバウンドなPythonコードとI/Oバウンドな処理を区別し、同一のワークロードを使用してスレッドプール、インタプリタプール、プロセスプールを比較します。また、障害の封じ込めと決定論的なシャットダウンについても計画します。
最初に確認すべき質問
- ワークロードはPythonバイトコード、GILを解放するネイティブコード、またはI/Oのどれですか?
- 引数と戻り値は低コストでシリアライズ可能ですか?また、オブジェクトは巨大または共有されていますか?
- タスクはプロセスグローバルなキャッシュ、オープンなソケット、または変更可能なモジュール状態に依存していますか?
- レイテンシ、スループット、メモリ、起動時間の上限はどの程度ですか?
- イニシャライザやワーカーの障害はキュー内のジョブにどのように影響すべきですか?
- デプロイ環境はPython 3.14およびそのプールセマンティクスと互換性がありますか?
30秒の回答フレームワーク
「スレッド、InterpreterPoolExecutor、プロセスの3つのコントロールでベンチマークを実施します。インタプリタはマルチコア上でPythonバイトコードを実行できますが、各ワーカーは分離されており、送信される呼び出し可能オブジェクト、引数、結果にはシリアライズが必要です。そのコストを償却できる程度にタスクを粗粒度に保ち、イニシャライザでリソースを作成し、共有される変更可能なグローバル変数を避け、キャンセルと再起動の挙動を定義します。最終判断には、スループット、p95レイテンシ、CPU、メモリ、シリアライズ時間、障害率のエビデンスが必要です。」
ステップ別の回答
ステップ1: ワークロードを分類する
CPU時間、GILの競合、ネイティブ拡張の時間、ブロッキングI/O、タスク実行時間を測定します。I/OやGILを解放するライブラリにはスレッドで十分な場合があります。インタプリタはPythonレベルのCPU並列処理を対象とします。プロセスは依然として有用な分離と互換性のコントロールとして機能します。
ステップ2: シリアライズ境界を設計する
トップレベルでインポート可能な呼び出し可能オブジェクトとコンパクトなデータ値を渡します。クロージャ、オープンされたファイルディスクリプタ、ロック、巨大なオブジェクトグラフは避けます。シリアライズが支配的である場合は、レコードをバッチ処理するか、不変データを繰り返しコピーする代わりに共有外部ストアに移動します。
ステップ3: インタプリタごとのリソースを初期化する
イニシャライザを使用して、モジュールのインポート、決定論的な状態の設定、およびそのインタプリタにローカルなクライアントの作成を行います。モジュールレベルのシングルトンがワーカー間で共有されると想定してはいけません。ユーザーデータを漏洩させることなく、プールとインタプリタのIDを診断情報に記録します。
ステップ4: 障害とキャンセルを定義する
イニシャライザの障害はプールレベルのイベントとして扱い、キュー内のタスクの挙動を明確にします。未完了のFutureに上限を設け、タスクIDとともに例外を伝播させ、バッチ境界で処理をキャンセルします。ワーカーの再起動時には、インタプリタローカルなリソースを再作成し、冪等でない副作用を重複させてはなりません。
ステップ5: ベンチマークとロールアウト
スレッド、インタプリタ、プロセスプール間で、同一のタスクサイズと並行性を比較します。起動時間、シリアライズ時間、計算時間、結果マージ時間に加え、RSS、スループット、p95、例外、シャットダウン時間を記録します。1つのワークロードでカナリアリリースを実施し、プロセスプールへのフォールバックを維持し、分離やメモリのコストがCPUの利得を相殺する場合は中止します。
モデル回答
「InterpreterPoolExecutorは、GILを解放するネイティブライブラリに依存できないCPU負荷の高いPythonコードの有力な候補です。まずワークロードの性質を検証し、同一の入力の下でスレッド、インタプリタ、プロセスを比較します。タスクはシリアライズ境界を越える必要があるため、インポート可能な呼び出し可能オブジェクト、コンパクトなバッチ、インタプリタローカルな初期化を使用し、共有される変更可能なグローバル変数は排除します。イニシャライザの失敗、キャンセル、冪等性、シャットダウンを定義した上で、スループット、p95、RSS、シリアライズオーバーヘッド、障害のエビデンスを基準にロールアウトを制御し、プロセスへのフォールバックを用意します。」
よくある間違い
- インタプリタ間でグローバル変数が共有されると想定する → 状態は分離されており初期化が繰り返される → インタプリタごとにリソースを作成する。
- 極小のタスクを送信する → シリアライズとスケジューリングが支配的になる → 処理をバッチ化してオーバーヘッドを測定する。
- pickle化できないオブジェクトを渡す → 実行時に送信が失敗する → インポート可能な呼び出し可能オブジェクトと単純な値を使用する。
- I/Oにインタプリタを使用する → 複雑化するだけでCPUの利点が得られない → まずスレッドを検討する。
- リトライ時の副作用を無視する → 再起動によって書き込みが重複する → タスクを冪等にするか外部コミットを使用する。
- スループットのみを測定する → メモリやテールレイテンシの悪化が見落とされる → RSS、p95、シャットダウン時間を追跡する。
フォローアップの質問
フォローアップ 1: インタプリタはどのように並列処理を実現しますか?
各ワーカーは独自のインタプリタとGILを持っているため、Pythonバイトコードをマルチコアで実行できます。ワーカー同士は通常のインタプリタ状態を共有しません。
フォローアップ 2: プロセスが好ましいのはどのような場合ですか?
プロセスの起動コストやメモリコストよりも、強力なアドレス空間の分離、既存のプロセス対応ライブラリ、またはシンプルなデプロイセマンティクスが重視される場合です。
フォローアップ 3: モジュールの状態はどうなりますか?
インポートおよび変更可能なモジュールグローバル変数はインタプリタローカルです。必要な状態は各ワーカー内で初期化し、送信側インタプリタで作成されたシングルトンに決して依存しないでください。
フォローアップ 4: タスクの粒度はどのように選択しますか?
シリアライズとスケジューリングの時間が実行時間のごく一部になるまでバッチサイズを増やし、そのバッチがレイテンシ、キャンセル、リトライの要件を満たしていることを検証します。