2024年12月08日
おはようございます!AI/BI部のCです。
最近Advent Calendarイベントが行われており、仕事でPythonのasyncioライブラリを使用する際にいくつかの問題に遭遇しました。問題解決のプロセスとその後の考察を皆さんと共有します。
ご存知の通り、Pythonの並行プログラミングは長い間批判されてきました。例えば、Pythonのマルチスレッドは実際には真のマルチスレッドではなく、GILを使用してスレッドを切り替えています。CPUの複数のコアを本当に活用できるのはマルチプロセスだけです。しかし、2014年のPython 3.4以降、asyncio
モジュールが導入され、スレッドはPythonの非同期プログラミングの中心となりました。
早期のバージョン(Python 3.7以前)では、コルーチンの呼び出しには明示的にイベントマネージャを制御する必要がありました。その時のコードはおおよそ以下のような感じです。
get_event_loop
関数は明示的にイベントループを作成し、その後 run_until_complete
でコルーチンを登録して実行します。しかし、Python 3.7以降では run
メソッドが導入され、イベントループの作成と管理が自動的に行われるようになり、使用方法は次のように変わりました。以前よりシンプルになりました。
import asyncio import aiohttp asyncdefmain(): async with aiohttp.ClientSession() as client: resp = awaitclient.get('http://httpbin.org/ip') ip = awaitresp.json() print(ip) asyncio.run(main())
RuntimeError
が発生する理由を考えてみましょう。まず、asyncio
の公式ドキュメントによると、asyncio.run()
は他のイベントループが実行中のときには実行できないと記載されています。asyncio.run()
によって自動的に作成されたイベントループ以外で、どこでイベントループが作成されているのでしょうか。ここで motor.motor_asyncio.AsyncIOMotorClient
のソースコードを確認してみると、イベントループの引数を渡すことができ、渡さない場合には新たにイベントループを作成することが分かります。これにより、__init__
関数内で新しいイベントループが作成され、その結果後続の asyncio.run()
がエラーを起こすことになります。この問題を解決する方法は2つあります。一つは、外部で asyncio.get_event_loop()
を使用して、現在のスレッドのイベントループ(つまり、motor
が作成したイベントループ)を取得することです。これにより、複数のイベントループが存在することを避けることができます。
もう一つの方法は、motor
パッケージをアップグレードすることです。バージョン 3.0 以降では、AsyncIOMotorClient
の初期化時にイベントループを自動的に作成しなくなったため、衝突が発生しなくなります。
このエラーについてさらに asyncio
の仕組みを掘り下げていくと、コルーチンにとってイベントループはスケジューラのような役割を果たしていることがわかります。コルーチンは awaitable
特性を持ち、await
キーワードで自らを一時停止させ、制御をイベントループに返すことができます。これにより、イベントループは他のコルーチンをスケジュールして実行することができ、並行処理が実現されます。これらはすべて1つのスレッド内で行われ、スレッド間のコンテキストスイッチが不要なため、コルーチンは軽量で効率的な並行処理の手段となります。
では、最後に、マルチスレッド環境においてコルーチン同士はどのように管理されるのでしょうか?それらは完全に独立したリソースなのでしょうか?この点については、後続の記事でさらに詳しく議論していきたいと思います。