できない.dev

Python の asyncio で「RuntimeError: Event loop is closed」が解消できない

`RuntimeError: Event loop is closed` は asyncio のイベントループを閉じた後にコルーチンやコールバックが実行されようとした際に発生します。
`asyncio.run()` に任せ、Windows では `WindowsSelectorEventLoopPolicy` に切り替えるのが定番の対処です。

#asyncio#event-loop#runtimeerror#aiohttp

公開:

要約

RuntimeError: Event loop is closed は asyncio のイベントループを閉じた後にコルーチンやコールバックが走ろうとした際の例外です。
多くは Windows + aiohttp の組み合わせ、もしくは loop.close() を自前で呼んだケースで再現します。asyncio.run() に任せ、Windows では WindowsSelectorEventLoopPolicy に切り替えるのが定番の対処です。

よくある原因

  1. Windows + Python 3.8 以降のデフォルト ProactorEventLoop と aiohttp の SSL クローズ処理の組み合わせ
  2. asyncio.run() の後にさらに loop.close() を呼んでいる
  3. プログラム終了直前に SSL transport などのファイナライザがクローズ済みループへ await する
  4. Jupyter Notebook 内で asyncio.run() を呼び、既存ループと衝突している

解決策

1. Windows ではセレクタ系ポリシーに切り替える

aiohttp の README にも記載のある既知の回避策です。asyncio.run() を呼ぶ前に一度だけポリシーを設定します(公式ドキュメント)。

import asyncio
import sys
 
if sys.platform == "win32":
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
 
asyncio.run(main())

2. ループを自前で閉じない

loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
asyncio.run(other())

上の書き方は asyncio.run が新しいループを作るタイミングで閉じ忘れの transport を踏みます。
代わりに asyncio.run だけに統一します。

asyncio.run(main())
asyncio.run(other())

3. 残タスクを掃除してから終了する

async def main():
    await do_work()
    pending = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
    for t in pending:
        t.cancel()
    await asyncio.gather(*pending, return_exceptions=True)
 
asyncio.run(main())

4. Jupyter / IPython では asyncio.run を使わない

Jupyter のセルでは既にイベントループが回っているため、asyncio.run() は失敗します。
トップレベルで await main() と書くか、どうしても asyncio.run() を使いたければ nest_asyncio.apply() で入れ子を許可します。

この記事は役立ちましたか?