モンキーパッチング対AsyncAwait Pythonの並行処理パラダイム2つの物語
Wenhao Wang
Dev Intern · Leapcell

汎用性と可読性で称賛されるPythonは、現代のソフトウェア開発の需要を満たすために常に進化しています。同時実行性、つまり複数のタスクを同時に実行しているように見える機能は、特に今日のデータ駆動型でネットワーク集約型の世界において、応答性の高いパフォーマンスの高いアプリケーションを構築する上で重要な側面です。長年にわたり、Python開発者は同時実行性を達成するためにさまざまな戦略を採用してきました。その中でも、モンキーパッチングの動的で実行時コードを変更する力と、async/await構文が提供するより構造化された明示的な制御という、非常に異なる2つの哲学が登場しました。この記事では、これらの2つのパラダイムを探り、それらの基盤となるメカニズム、典型的なアプリケーション、およびPythonでの同時実行操作のいずれかを選択する際のトレードオフを対比させます。これらの違いを理解することは、アプリケーションの保守性、スケーラビリティ、堅牢性に影響を与える情報に基づいたアーキテクチャ上の意思決定を行う上で重要です。
モンキーパッチングとAsyncAwaitの世界
直接比較に入る前に、関連するコアコンセプトを明確に理解しましょう。
コア用語
- 並行処理 (Concurrency): 一度に複数のタスクを処理する能力。タスクが同時に実行されている(並列性)ことを必ずしも意味するのではなく、ある期間に複数のタスクで進歩できることを意味します。
- モンキーパッチング (Monkey Patching): プログラムの元のソースコードを変更せずに、実行時コードを拡張または変更する手法。通常、実行時にメソッド、クラス、またはモジュール全体を置き換えることを含みます。
async/await: コルーチン(一時停止および再開可能な関数)を定義および実行するためのPythonの組み込み構文。このノンブロッキングI/Oアプローチは、非同期プログラミングの中心であり、単一スレッドが複数のI/Oバウンド操作を効果的に管理できるようにします。- イベントループ (Event Loop):
asyncioおよび同様の非同期フレームワークのコア。コルーチンのスケジュールと実行、I/O操作の管理、イベントのディスパッチを担当します。 - コルーチン (Coroutines): Pythonの特別な関数(
async defで定義)で、実行を一時停止し、イベントループに制御を戻すことができ、他のタスクを実行できるようにします。それらが待機していたI/O操作が完了すると、再開できます。
並行処理のためのモンキーパッチング
並行処理のためのモンキーパッチングは、通常、geventやeventletのようなライブラリを使用します。これらのライブラリは、標準ライブラリ関数(socketやtime.sleepのようなI/O操作)を「協調的にスケジューリングされる」ようにパッチすることで、並行処理を実現します。パッチされたI/O操作が呼び出されると、スレッド全体をブロックする代わりに、geventまたはeventletスケジューラに制御が譲渡され、次に別の「グリーンレット」(これらのライブラリによって提供される軽量コルーチン)に切り替わります。
仕組み:
- 標準ライブラリをインポートして「モンキーパッチ」します。
# gevent の例 from gevent import monkey monkey.patch_all() # socket, ssl, threading, time など標準ライブラリモジュールをパッチします。 import gevent import requests # 内部でgevent対応ソケットを使用するようになります - I/O操作を実行する関数を通常どおり定義します。特別な
asyncまたはawaitキーワードは必要ありません。 gevent.spawnまたはeventlet.spawnを使用してグリーンレットを作成し、gevent.joinallを使用してそれらを待ちます。
例: geventを使用して複数のURLを並行して取得する:
# gevent_example.py from gevent import monkey import gevent import time import requests # 標準ライブラリ(例:socket, time)をパッチ monkey.patch_all() def fetch_url(url): print(f"Fetching starting: {url}") try: response = requests.get(url) print(f"Fetching finished: {url}, Status: {response.status_code}") return len(response.content) except Exception as e: print(f"Error fetching {url}: {e}") return 0 urls = [ "http://www.google.com", "http://www.yahoo.com", "http://www.bing.com", "http://www.python.org", ] if __name__ == "__main__": start_time = time.time() # 各URLのグリーンレットを生成 geenlets = [gevent.spawn(fetch_url, url) for url in urls] # 全てのグリーンレットが完了するのを待つ gevent.joinall(geenlets) total_bytes = sum(g.value for g in geenlets) end_time = time.time() print(f"\nTotal bytes fetched: {total_bytes}") print(f"Total time taken: {end_time - start_time:.2f} seconds")
この例では、requests.get()は通常ブロックします。しかし、monkey.patch_all()の後、基盤となるソケット操作はノンブロッキングになり、制御を譲渡するため、他のfetch_url呼び出しが同じスレッドで並行して進行できるようになります。
アプリケーションシナリオ:
- 最小限のコード変更で、既存のブロッキングコードを並行モデルに移行する。
async/awaitをサポートしないレガシーライブラリとの統合。- 多数の同時接続を処理するためのWebサーバー(Gunicornが
geventまたはeventletワーカーを使用)。
Async/Awaitの世界
async/awaitは、Python 3.5のasyncioライブラリで導入された、非同期プログラミングのためのPythonの明示的なネイティブサポートです。イベントループを使用した協調的マルチタスクの原則に基づいて動作します。async defでマークされた関数はコルーチンであり、awaitキーワードを使用して実行を一時停止する可能性のあるポイントを明示的に示します。
仕組み:
async defを使用してコルーチンを定義します。- コルーチンの内部で、
awaitを使用して、awaitable(別のコルーチン、Future、またはタスク)が完了するまで実行を一時停止します。 - イベントループがこれらのコルーチンを実行し、1つがI/O操作を待機するときにそれらを切り替えます。
例: asyncioとhttpxを使用して複数のURLを並行して取得する:
# asyncio_example.py import asyncio import httpx # 非同期対応HTTPクライアント import time async def fetch_url_async(client, url): print(f"Fetching starting: {url}") try: response = await client.get(url) # 非同期HTTP GETリクエストを待機 print(f"Fetching finished: {url}, Status: {response.status_code}") return len(response.content) except Exception as e: print(f"Error fetching {url}: {e}") return 0 async def main(): urls = [ "http://www.google.com", "http://www.yahoo.com", "http://www.bing.com", "http://www.python.org", ] start_time = time.time() async with httpx.AsyncClient() as client: # 非同期HTTPクライアントを使用 tasks = [fetch_url_async(client, url) for url in urls] # タスクを並行して実行し、すべて完了するのを待つ results = await asyncio.gather(*tasks) total_bytes = sum(results) end_time = time.time() print(f"\nTotal bytes fetched: {total_bytes}") print(f"Total time taken: {end_time - start_time:.2f} seconds") if __name__ == "__main__": asyncio.run(main()) # メインコルーチンを実行
ここでは、httpx.AsyncClient().get()はawaitableです。await client.get(url)が呼び出されると、fetch_url_asyncは一時停止し、イベントループは他の保留中のタスクに切り替えたり、新しいgetリクエストを並行して開始したりできます。
アプリケーションシナリオ:
- 高性能ネットワークアプリケーション(Webサーバー、API、チャットアプリケーション)の構築。
- データベースおよびマイクロサービスが他の非同期サービスを消費する。
- 並行処理フローの明示的な制御が望ましいI/Oバウンドアプリケーション。
- FastAPIやStarletteのような最新のWebフレームワークは、すべて
asyncioを中心に構築されています。
比較とトレードオフ
| 特徴 | モンキーパッチング(例:gevent) | Async/Await (asyncio) | ---
| 明示性 | 暗黙的:標準のブロッキング呼び出しがノンブロッキングになります。 | 明示的:asyncおよびawaitキーワードが並行処理を明確にマークします。 | ---
| 侵襲性 | 非常に侵襲的:実行時にグローバル状態と標準ライブラリを変更します。 | 非侵襲的:明示的なasync APIに依存します。 | ---
| 学習曲線 | 既存のブロッキングコードには低いですが、パッチングの理解は複雑になる可能性があります。
| 初心者には高い、イベントループとコルーチンの理解が必要です。 | ---
| エコシステムサポート|ニッチ、ライブラリのパッチ適用バージョンに依存。すべてのライブラリがパッチ可能ではありません。
急速に成長しており、多くの最新ライブラリはネイティブのasyncioです。 |
|---|
| デバッグ |
| 明示的な制御により、通常、より明確なエラーメッセージとスタックトレースが得られます。 |
| --- |
| パフォーマンス |
| I/Oバウンドタスクに優れています。ネイティブサポートは最適化の可能性を提供します。 |
| --- |
| 互換性 |
ライブラリがasyncio互換であるか、asyncラッパーを提供する必要があります。 |
| --- |
| メンタルモデル |
モンキーパッチングが輝く場所:
- レガシーコード移行: 従来のブロッキング呼び出しで書かれた既存のコードベースが膨大な場合、モンキーパッチングは、大規模な書き直しなしに並行処理を導入する簡単な方法を提供できます。
- 迅速なプロトタイピング:
asyncioのために書き直すオーバーヘッドが高すぎると判断された、非同期でないアプリケーションに並行処理を迅速に追加する場合。
Async/Awaitが輝く場所:
- 新規プロジェクト: 明示的な並行処理モデルが好まれる、最新の高性能アプリケーションをゼロから構築する場合。
- 堅牢性と保守性:
async/awaitの明示的な性質により、コードの推論、理解、デバッグが容易になり、長期的にはより保守性の高いアプリケーションにつながります。 - 最新のエコシステム:
asyncioネイティブライブラリやフレームワークの成長するエコシステムとのシームレスな統合。 - 明確な制御フロー: 開発者は、コンテキストスイッチが発生するタイミングをより細かく制御できます。
結論
モンキーパッチング(主にgeventのようなライブラリ経由)とasync/awaitの両方で、Pythonで並行処理を達成するための強力な方法が提供されます。モンキーパッチングは、既存のブロッキングコードにほぼ魔法のような移行を提供し、最小限の構文変更で並行実行できるようにしますが、グローバル状態の変更とデバッグの頭痛の種を犠牲にします。対照的に、async/awaitは、非同期操作を明確にマークすることで、より明示的で意図的な並行プログラミングアプローチを必要とし、特に新規プロジェクトや完全な書き直しにおいて、より堅牢で保守性が高く最新のコードにつながります。これら2つのパラダイムの選択は、特定のプロジェクトのニーズ、既存のコードベース、およびチームの明示的な非同期プログラミングに対する快適さに依存しますが、トレンドは明らかにasync/awaitの明示的な制御と成長するエコシステムを支持しています。グリーンフィールドプロジェクトにとって、スケーラブルで透過的な並行Pythonアプリケーションのパスを確立するために、async/awaitを採用することは間違いなく前進する道です。

