設問とコンテキスト
ある非同期サービスでは、I/O を行わずにインメモリキャッシュからリクエストを処理することが多いものの、すべてのコルーチンが依然としてタスクとなり、イベントループのスケジューリングを待機します。チームは asyncio.eager_task_factory をグローバルに設定することを提案しています。トレードオフを評価し、安全なロールアウトを提案してください。
Python のドキュメントによると、eager 実行は Task の構築中にコルーチンを同期的に開始し、コルーチンがブロックされた場合にのみ Task がスケジューリングされます。この API は Python 3.12 で追加され、観測可能なスケジューリングセマンティクスを変更します。
面接官が見ているポイント
重要なのは、同期的な完了と非同期のブロッキングを区別し、公平性(fairness)、順序、例外の発生タイミング、キャンセル、および TaskGroup への影響を説明することです。優れた回答は、最適化を計測された境界に限定し、バージョンチェック、フィーチャーフラグ、ロールバック、およびイベントループの指標を含めます。
最初に確認すべき明確化の質問
- すべてのデプロイ環境における最小 Python バージョンは何ですか?
- これらのコルーチンはメモリキャッシュの読み取りですか、それともネットワーク、データベース、ファイル、またはロックの I/O を実行する可能性がありますか?
- コードはタスク生成の順序、ループの公平性、または
call_soonの順序に依存していますか? - 障害、キャンセル、およびデッドラインは
TaskGroupまたはリクエストスコープによって所有されていますか? - 目標はタスク作成の CPU、テールレイテンシ、スループットのどれであり、ベースラインは何ですか?
30秒の回答フレームワーク
「最初からグローバルに有効化することはしません。eager factory は Task の構築中にコルーチンを開始するため、キャッシュヒットによってイベントループのスケジューリングターンを1回回避できます。ブロックするコルーチンは通常どおりスケジューリングされます。これにより順序、例外のタイミング、公平性が変化します。同期タスクを大量に生成するループは、タイマーや他のリクエストを遅延させる可能性があります。私なら Python のバージョンを確認し、測定されたキャッシュ呼び出し箇所でロールアウトし、イベントループの遅延とテールレイテンシを比較し、即時ロールバック用にデフォルトのファクトリを保持します。」
ステップごとの詳細解説
ステップ 1: デフォルトモデルと eager モデルを提示する
デフォルトのファクトリでは、create_task はコルーチンをすぐに実行するようにスケジューリングします。eager factory では、構築時にコルーチンがリターンするか、例外を送出するか、または最初のブロッキング await に達するまで即座に進められます。同期的な完了の場合、イベントループのキューにまったく入らない可能性があります。
loop.set_task_factory(asyncio.eager_task_factory)
task = asyncio.create_task(read_cached(key))したがって、eager 実行は単にスケジューラが高速化されるだけでなく、観測可能なセマンティクスの変更を伴います。
ステップ 2: コルーチンの境界を選択する
適しているのは、短時間でヒット率の高いインメモリキャッシュやメモ化された操作です。ネットワーク、データベース、ファイル、ロック、または制限のない CPU 処理を実行する可能性があるコルーチンは、構築処理が現在のタスクを独占しないよう、予測可能なブロッキング境界を維持すべきです。
ステップ 3: 順序と公平性を分析する
ループ内でタスクが作成される場合、eager コルーチンは作成順に即座に完了する可能性があり、以前はイベントループに依存していたインターリーブ(交互実行)が変化します。大規模な同期的バッチは、タイマー、I/O コールバック、および他のリクエストを遅延させる可能性があります。イベントループの遅延を追跡し、バッチサイズを制限してください。
ステップ 4: 例外とキャンセルを処理する
最初のブロックの前に送出された例外は create_task の近くで発生する可能性があり、捕捉ポイントとスタックの形状が変化します。Task の参照を保持し、クリーンアップ後に CancelledError が伝播するようにしてください。リクエストのキャンセルやキャッシュの失敗を成功レスポンスに変えるために eager モードを使用してはなりません。
ステップ 5: TaskGroup およびデッドラインと組み合わせる
TaskGroup は引き続きタスクツリー、兄弟タスクのキャンセル、および join を所有しますが、子タスクは create_task の間にすでに完了または失敗している可能性があります。構造化された try/except* 処理と1つの外部デッドラインを使用してください。すべての子タスクが最初にキューで待機すると想定してはなりません。
async with asyncio.TaskGroup() as group:
user = group.create_task(read_cached("user"))
orders = group.create_task(read_remote("orders"))ステップ 6: バージョンを確認し、機能のスコープを限定する
このファクトリは Python 3.12 から利用可能です。Python 3.14 では create_task に eager_start オプションも公開されています。複数バージョンが存在するサービスでは、起動時にランタイムを検証し、無条件のグローバルなファクトリ変更よりも局所的な呼び出し箇所での実験を優先すべきです。
ステップ 7: ロールバック指標を用意してロールアウトする
スイッチで制御されたキャッシュ専用の呼び出し箇所から開始します。デフォルトのファクトリと比較して、CPU、タスク構築時間、キャッシュヒットのテールレイテンシ、イベントループの遅延、例外発生率、キャンセル率、およびダウンストリームの QPS を比較します。公平性やエラーが悪化した場合は、ビジネスコードを変更することなくデフォルトに戻します。
ステップ 8: ベンチマークだけでなくセマンティクスをテストする
同期的リターン、同期的例外送出、最初の await、外部キャンセル、複数の TaskGroup 障害、デッドライン、再帰的なタスク作成、タイマーの公平性、およびキャッシュとリモートが混在するバッチを網羅します。イベントを記録して順序をアサートします。スループットのベンチマークだけではスケジューリングセマンティクスを検証できません。
模範的な高評価の回答
「eager 実行はスケジューリングターンを省略できるため、短く予測可能なキャッシュコルーチンに適しています。予測できない I/O や長い CPU パスに対してはリスクがあります。主なリスクは、順序、公平性、および例外タイミングの変化です。私ならバージョンを確認し、フラグ配下で局所的に有効化し、デフォルトへのロールバック手段を保持しつつ、イベントループの遅延、テールレイテンシ、キャンセル、およびエラーを監視します。TaskGroup と共有デッドラインはそのまま維持し、通常のキャンセルとクリーンアップも維持します。」
よくある間違い
- eager モードをセマンティクスに影響がないものとして扱う → 順序と例外のタイミングが変化する → 両方の実行モデルを書き出し、測定する。
- すべてのコルーチンで有効にする → 低速な I/O や CPU 処理が現在のタスクをブロックする → 短い同期パスのみをカナリアリリースする。
- Python のバージョンを無視する → 古いデプロイ環境で起動時に失敗する → ランタイムを検証し、デフォルトのファクトリを保持する。
- 平均スループットのみに注目する → 公平性が知らぬ間に悪化する → ループ遅延とテール指標を追加する。
- TaskGroup が不変であると仮定する → 構築時の障害が誤って処理される → eager な子タスクの障害と例外グループをテストする。
- キャンセルやクリーンアップのエラーを握りつぶす → リクエストの処理がリークする → キャンセルを保持し、解放パスを検証する。
フォローアップの質問と優れた回答
フォローアップ 1: 同期的なキャッシュヒットでも Task は残りますか?
Task は構築中にすでに完了している可能性があります。イベントループのターンを必須とせず、返されたオブジェクトを読み取り、eager 完了パスを明示的にテストしてください。
フォローアップ 2: eager モードは完了のための作成順序を保証しますか?
いいえ。開始タイミングが変わるだけで、コルーチンが一度ブロックされると通常のスケジューリングが適用されます。ビジネス上の順序が重要な場合は、明示的に調整するか、操作キーで結果をソートしてください。
フォローアップ 3: キャッシュヒットのバッチがリクエストを飢餓状態(スタベーション)にするのを防ぐにはどうすればよいですか?
バッチサイズを制限し、必要に応じてバッチ間で yield し、ループ遅延を監視します。コストの低い1つの呼び出し箇所に eager モードを適用する方が、グローバルなポリシーよりも安全です。
フォローアップ 4: リグレッション発生後、どのようにロールバックしますか?
スイッチを無効化し、デフォルトのタスクファクトリを復元し、メトリクスがベースラインに戻ることを確認し、診断のためにランタイムバージョンとイベント順序を含むトレースを保持します。
フォローアップ 5: eager_start はファクトリとどのように関連していますか?
eager_start は単一の Task に対する明示的なオプションであり、ファクトリはイベントループのデフォルトポリシーです。両者を組み合わせる前に、正確な Python バージョンのシグネチャと優先順位を確認してください。