Python 3.5 では、同時実行性を処理するスレッドの代替として非同期 I/O が導入されました。非同期 I/O と Python での asyncio 実装の利点は、メモリを大量に消費するオペレーティング システム スレッドを生成しないため、システムの使用リソースが減り、スケーラビリティが向上することです。さらに、asyncio では、スケジュール ポイントは await
構文によって明確に定義されますが、スレッドベースの同時実行では、GIL が予測できないコード ポイントで解放される可能性があります。その結果、asyncio ベースの同時実行システムは理解とデバッグが容易になります。最後に、asyncio タスクをキャンセルできますが、これはスレッドを使用する場合は簡単ではありません。
ただし、これらの利点を真に活用するには、非同期コルーチンでの呼び出しのブロックを避けることが重要です。ブロック呼び出しには、ネットワーク呼び出し、ファイル システム呼び出し、sleep
呼び出しなどがあります。 asyncio は内部でシングルスレッドのイベント ループを使用してコルーチンを同時に実行するため、これらのブロック呼び出しは有害です。したがって、コルーチン内でブロッキング呼び出しを行うと、イベント ループ全体とすべてのコルーチンがブロックされ、アプリケーションの全体的なパフォーマンスに影響します。
次に、コードの同時実行を防ぐブロック呼び出しの例を示します。
<code class="language-python">import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") time.sleep(1) # time.sleep 是一个阻塞函数 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())</code>
実行結果は次のようになります:
<code>2025-01-07 18:50:15.327677: 1 start 2025-01-07 18:50:16.328330: 1 stop 2025-01-07 18:50:16.328404: 2 start 2025-01-07 18:50:17.333159: 2 stop</code>
ご覧のとおり、2 つのコルーチンは同時に実行されていません。
この問題を解決するには、非ブロッキング同等のものを使用するか、実行をスレッド プールに延期する必要があります。
<code class="language-python">import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") await asyncio.sleep(1) # 将阻塞的 time.sleep 调用替换为非阻塞的 asyncio.sleep 协程 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())</code>
実行結果は次のようになります:
<code>2025-01-07 18:53:53.579738: 1 start 2025-01-07 18:53:53.579797: 2 start 2025-01-07 18:53:54.580463: 1 stop 2025-01-07 18:53:54.580572: 2 stop</code>
ここでは 2 つのコルーチンが同時に実行されます。
問題は、メソッドがブロックしているかどうかを特定するのが必ずしも簡単ではないことです。特にコードベースが大きい場合やサードパーティのライブラリを使用している場合はそうです。場合によっては、コードの深い部分でブロッキング呼び出しが行われることがあります。
たとえば、このコードはブロックしますか?
<code class="language-python">import blockbuster from importlib.metadata import version async def get_version(): return version("blockbuster")</code>
Python は起動時にパッケージのメタデータをメモリにロードしますか? blockbuster
モジュールがロードされたときに完了しますか?それとも version()
を呼び出すときでしょうか?結果はキャッシュされますか? 後続の呼び出しはノンブロッキングになりますか?正しい答えは、version()
を呼び出すときに行われます。これには、インストールされたパッケージの METADATA ファイルの読み取りが含まれます。そして結果はキャッシュされません。したがって、version()
はブロック呼び出しであり、常にスレッドに延期される必要があります。 importlib
のコードを詳しく調べずにこの事実を知るのは困難です。
ブロッキング コールを検出する 1 つの方法は、asyncio のデバッグ モードをアクティブにして、時間がかかりすぎるブロッキング コールをログに記録することです。ただし、これは最も効率的なアプローチではありません。トリガー タイムアウトよりも短いブロック時間が多くてもパフォーマンスに悪影響を及ぼし、テスト/開発でのブロック時間は運用環境とは異なる可能性があるためです。たとえば、データベースが大量のデータをフェッチする必要がある場合、運用環境ではデータベースの呼び出しに時間がかかることがあります。
ここでブロックバスターの出番です! BlockBuster をアクティブにすると、asyncio イベント ループから呼び出された場合にエラーをスローするいくつかのブロッキング Python フレームワーク メソッドにパッチが適用されます。デフォルトのパッチ適用メソッドには、os
、io
、time
、socket
、および sqlite
モジュールのメソッドが含まれます。 BlockBuster によって検出されたメソッドの完全なリストについては、プロジェクトの Readme を参照してください。その後、単体テスト モードまたは開発モードで BlockBuster をアクティブにして、ブロックされている呼び出しを捕捉して修正できます。 JVM 用の素晴らしい BlockHound ライブラリをご存知の場合は、Python でも同じ原理です。クリエイターのおかげで、BlockHound は BlockBuster の素晴らしいインスピレーションの源でした。
上記のブロック コード スニペットで BlockBuster を使用する方法を見てみましょう。
まず、blockbuster
パッケージをインストールする必要があります
<code class="language-python">import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") time.sleep(1) # time.sleep 是一个阻塞函数 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())</code>
その後、pytest フィクスチャと blockbuster_ctx()
メソッドを使用して、各テストの開始時に BlockBuster をアクティブ化し、ティアダウン中に非アクティブ化することができます。
<code>2025-01-07 18:50:15.327677: 1 start 2025-01-07 18:50:16.328330: 1 stop 2025-01-07 18:50:16.328404: 2 start 2025-01-07 18:50:17.333159: 2 stop</code>
これを pytest で実行すると、次の結果が得られます
<code class="language-python">import asyncio import datetime import time async def example(name): print(f"{datetime.datetime.now()}: {name} start") await asyncio.sleep(1) # 将阻塞的 time.sleep 调用替换为非阻塞的 asyncio.sleep 协程 print(f"{datetime.datetime.now()}: {name} stop") async def main(): await asyncio.gather(example("1"), example("2")) asyncio.run(main())</code>
注: 通常、実際のプロジェクトでは、
blockbuster()
フィクスチャはconftest.py
ファイルでセットアップされます。
BlockBuster は asyncio プロジェクトで非常に役立つと思います。これは、私が取り組んだプロジェクトで多くのブロッキング コールの問題を検出するのに役立ちました。しかし、それは万能薬ではありません。特に、一部のサードパーティ ライブラリは、ネットワークまたはファイル システムと対話するために Python フレームワーク メソッドを使用せず、代わりに C ライブラリをラップします。これらのライブラリについては、テスト セットアップにルールを追加して、これらのライブラリへの呼び出しのブロックをトリガーできます。 BlockBuster はオープン ソースでもあります。コア プロジェクトにお気に入りのライブラリのルールを追加するための貢献は大歓迎です。問題や改善の余地がある場合は、プロジェクトの問題トラッカーでフィードバックをお待ちしています。
いくつかのリンク:
以上がBlockBuster の紹介: asyncio イベント ループはブロックされていますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。