asyncio での開発

非同期プログラミングは伝統的な "同期的" プログラミングとは異なります。

このページはよくある間違いや落とし穴を列挙し、それらを回避する方法を説明します。

デバッグモード

asyncio はデフォルトで本運用モードで実行されます。いっぽう、開発を容易にするために asyncio は "デバッグモード" を持っています。

asyncio のデバッグモードを有効化する方法はいくつかあります:

デバッグモードを有効化することに加え、以下も検討してください:

  • asyncio ロガー のログレベルをlogging.DEBUG に設定します。例えばアプリケーションの起動時に以下を実行します:

    logging.basicConfig(level=logging.DEBUG)
  • warnings モジュールがResourceWarning 警告を表示するように設定します。やり方のひとつは-Wdefault コマンドラインオプションを使うことです。

デバッグモードが有効化されたときの動作:

  • asyncio checks forcoroutines that were not awaited and logs them; this mitigatesthe "forgotten await" pitfall.

  • スレッドセーフでない asyncio APIs の多く (loop.call_soon()loop.call_at() など) は、誤ったスレッドから呼び出されたときに例外を送出します。

  • I/O セレクタが I/O 処理を実行する時間が長すぎる場合、その実行時間が記録されます。

  • 実行時間が100ミリ秒を超えるコールバックは記録されます。 "遅い" の判断基準となる実行時間の最小値はloop.slow_callback_duration 属性で設定できます。

並行処理とマルチスレッド処理

イベントループはスレッド(典型的にはメインスレッド)内で動作し、すべてのコールバックとタスクをそのスレッド内で実行します。ひとつのタスクがイベントループ内で実行される間、他のタスクを同じスレッド内で実行することはできません。タスクがawait 式を実行すると、実行中のタスクはサスペンドされ、イベントループは次のタスクを実行します。

別の OS スレッドからのコールバック (callback) をスケジュールする場合、loop.call_soon_threadsafe() メソッドを使ってください。例:

loop.call_soon_threadsafe(callback,*args)

ほぼ全ての非同期オブジェクトはスレッドセーフではありませんが、タスクやコールバックの外側で非同期オブジェクトを使うコードが存在しない限り、それが問題にはなることはほとんどありません。もしそのような目的で低レベルの asyncio API を呼び出すようなコードを書く必要がある場合、loop.call_soon_threadsafe() メソッドを使ってください。例:

loop.call_soon_threadsafe(fut.cancel)

別の OS スレッドからコルーチンオブジェクトをスケジュールする場合は、run_coroutine_threadsafe() メソッドを使ってください。run_coroutine_threadsafe() は結果にアクセスするためのconcurrent.futures.Future オブジェクトを返します:

asyncdefcoro_func():returnawaitasyncio.sleep(1,42)# Later in another OS thread:future=asyncio.run_coroutine_threadsafe(coro_func(),loop)# Wait for the result:result=future.result()

シグナルの処理を行うには、イベントループはメインスレッド内で実行しなければなりません。

Theloop.run_in_executor() method can be used with aconcurrent.futures.ThreadPoolExecutor to executeblocking code in a different OS thread without blocking the OS threadthat the event loop runs in.

現在のところ、 (たとえばmultiprocessing で開始したような) 別のプロセスからコルーチンやコールバックを直接スケジュールすることはできません。イベントループのメソッド 節では、イベントループをブロックすることなくパイプからの読み込みやファイルデスクリプタの監視ができる API のリストを掲載しています。さらに、 asyncio のサブプロセス API はイベントループからプロセスを開始したりプロセスと通信したりする方法を提供します。 最後に、前述のloop.run_in_executor() メソッドはconcurrent.futures.ProcessPoolExecutor とともに使用することで、別のプロセス内でコードを実行することもできます。

ブロッキングコードの実行

ブロッキングコード (CPU バウンドなコード) を直接呼び出すべきではありません。たとえば、 CPU 負荷の高い関数を1秒実行したとすると、並行に処理されている全ての非同期タスクと I/O 処理は1秒遅れる可能性があります。

An executor can be used to run a task in a different thread or even ina different process to avoid blocking the OS thread with theevent loop. See theloop.run_in_executor() method for moredetails.

ログ記録

asyncio はlogging モジュールを利用し、 全てのログ記録は"asyncio" ロガーを通じて行われます。

デフォルトのログレベルはlogging.INFO ですが、これは簡単に調節できます:

logging.getLogger("asyncio").setLevel(logging.WARNING)

ネットワークログ記録は、イベントループをブロックし得ます。ログ処理のスレッドを分離するか、ノンブロッキング IO を使用することを推奨します。例えば、ブロックする handler を扱う を見てください。

待ち受け処理を伴わないコルーチンの検出

コルーチンが呼び出されただけで、待ち受け処理がない場合 (たとえばawaitcoro() のかわりにcoro() と書いてしまった場合) 、またはコルーチンがasyncio.create_task() を使わずにスケジュールされた場合、 asyncio はRuntimeWarning 警告を送出します:

importasyncioasyncdeftest():print("never scheduled")asyncdefmain():test()asyncio.run(main())

出力:

test.py:7:RuntimeWarning:coroutine'test'wasneverawaitedtest()

デバッグモードの出力:

test.py:7:RuntimeWarning:coroutine'test'wasneverawaitedCoroutinecreatedat(mostrecentcalllast)File"../t.py",line9,in<module>asyncio.run(main(),debug=True)<..>File"../t.py",line7,inmaintest()test()

通常の修正方法はコルーチンを待ち受ける (await) か、asyncio.create_task() 関数を呼び出すことです:

asyncdefmain():awaittest()

回収されない例外の検出

もしFuture.set_exception() メソッドが呼び出されても、その Future オブジェクトを待ち受けていなければ、例外は決してユーザーコードまで伝播しません。この場合 asyncio は、 Future オブジェクトがガベージコレクションの対象となったときにログメッセージを送出することがあります。

処理されない例外の例:

importasyncioasyncdefbug():raiseException("not consumed")asyncdefmain():asyncio.create_task(bug())asyncio.run(main())

出力:

Taskexceptionwasneverretrievedfuture:<Taskfinishedcoro=<bug()done,definedattest.py:3>exception=Exception('not consumed')>Traceback(mostrecentcalllast):File"test.py",line4,inbugraiseException("not consumed")Exception:notconsumed

タスクが生成された箇所を特定するには、デバッグモードを有効化して トレースバックを取得してください:

asyncio.run(main(),debug=True)

デバッグモードの出力:

Taskexceptionwasneverretrievedfuture:<Taskfinishedcoro=<bug()done,definedattest.py:3>exception=Exception('not consumed')createdatasyncio/tasks.py:321>source_traceback:Objectcreatedat(mostrecentcalllast):File"../t.py",line9,in<module>asyncio.run(main(),debug=True)<..>Traceback(mostrecentcalllast):File"../t.py",line4,inbugraiseException("not consumed")Exception:notconsumed