asyncio での開発
****************

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

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


デバッグモード
==============

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

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

* "PYTHONASYNCIODEBUG" 環境変数の値を "1" に設定する。

* Python 開発モード を使う。

* "asyncio.run()" 実行時に "debug=True" を設定する。

* "loop.set_debug()" を呼び出す。

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

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

     logging.basicConfig(level=logging.DEBUG)

* "warnings" モジュールが "ResourceWarning" 警告を表示するように設定し
  ます。やり方のひとつは "-W" "default" コマンドラインオプションを使う
  ことです。

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

* スレッドセーフでない 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"  オブジェクトを返します:

   async def coro_func():
        return await asyncio.sleep(1, 42)

   # Later in another OS thread:

   future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
   # Wait for the result:
   result = future.result()

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

The "loop.run_in_executor()" method can be used with a
"concurrent.futures.ThreadPoolExecutor" or "InterpreterPoolExecutor"
to execute blocking code in a different OS thread without blocking the
OS thread that the event loop runs in.

現在のところ、 (たとえば "multiprocessing" で開始したような) 別のプロ
セスからコルーチンやコールバックを直接スケジュールすることはできません
。 Event loop methods 節では、イベントループをブロックすることなくパイ
プからの読み込みやファイルデスクリプタの監視ができる 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, including
in a different interpreter, or even in a different process to avoid
blocking the OS thread with the event loop.  See the
"loop.run_in_executor()" method for more details.


ログ記録
========

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

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

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

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


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

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

   import asyncio

   async def test():
       print("never scheduled")

   async def main():
       test()

   asyncio.run(main())

出力:

   test.py:7: RuntimeWarning: coroutine 'test' was never awaited
     test()

デバッグモードの出力:

   test.py:7: RuntimeWarning: coroutine 'test' was never awaited
   Coroutine created at (most recent call last)
     File "../t.py", line 9, in <module>
       asyncio.run(main(), debug=True)

     < .. >

     File "../t.py", line 7, in main
       test()
     test()

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

   async def main():
       await test()


回収されない例外の検出
======================

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

処理されない例外の例:

   import asyncio

   async def bug():
       raise Exception("not consumed")

   async def main():
       asyncio.create_task(bug())

   asyncio.run(main())

出力:

   Task exception was never retrieved
   future: <Task finished coro=<bug() done, defined at test.py:3>
     exception=Exception('not consumed')>

   Traceback (most recent call last):
     File "test.py", line 4, in bug
       raise Exception("not consumed")
   Exception: not consumed

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

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

デバッグモードの出力:

   Task exception was never retrieved
   future: <Task finished coro=<bug() done, defined at test.py:3>
       exception=Exception('not consumed') created at asyncio/tasks.py:321>

   source_traceback: Object created at (most recent call last):
     File "../t.py", line 9, in <module>
       asyncio.run(main(), debug=True)

   < .. >

   Traceback (most recent call last):
     File "../t.py", line 4, in bug
       raise Exception("not consumed")
   Exception: not consumed


Asynchronous generators best practices
======================================

Writing correct and efficient asyncio code requires awareness of
certain pitfalls. This section outlines essential best practices that
can save you hours of debugging.


Close asynchronous generators explicitly
----------------------------------------

It is recommended to manually close the *asynchronous generator*. If a
generator exits early - for example, due to an exception raised in the
body of an "async for" loop - its asynchronous cleanup code may run in
an unexpected context. This can occur after the tasks it depends on
have completed, or during the event loop shutdown when the async-
generator's garbage collection hook is called.

To avoid this, explicitly close the generator by calling its
"aclose()" method, or use the "contextlib.aclosing()" context manager:

   import asyncio
   import contextlib

   async def gen():
     yield 1
     yield 2

   async def func():
     async with contextlib.aclosing(gen()) as g:
       async for x in g:
         break  # Don't iterate until the end

   asyncio.run(func())

As noted above, the cleanup code for these asynchronous generators is
deferred. The following example demonstrates that the finalization of
an asynchronous generator can occur in an unexpected order:

   import asyncio
   work_done = False

   async def cursor():
       try:
           yield 1
       finally:
           assert work_done

   async def rows():
       global work_done
       try:
           yield 2
       finally:
           await asyncio.sleep(0.1) # imitate some async work
           work_done = True


   async def main():
       async for c in cursor():
           async for r in rows():
               break
           break

   asyncio.run(main())

For this example, we get the following output:

   unhandled exception during asyncio.run() shutdown
   task: <Task finished name='Task-3' coro=<<async_generator_athrow without __name__>()> exception=AssertionError()>
   Traceback (most recent call last):
     File "example.py", line 6, in cursor
       yield 1
   asyncio.exceptions.CancelledError

   During handling of the above exception, another exception occurred:

   Traceback (most recent call last):
     File "example.py", line 8, in cursor
       assert work_done
              ^^^^^^^^^
   AssertionError

The "cursor()" asynchronous generator was finalized before the "rows"
generator - an unexpected behavior.

The example can be fixed by explicitly closing the "cursor" and "rows"
async-generators:

   async def main():
       async with contextlib.aclosing(cursor()) as cursor_gen:
           async for c in cursor_gen:
               async with contextlib.aclosing(rows()) as rows_gen:
                   async for r in rows_gen:
                       break
               break


Create asynchronous generators only when the event loop is running
------------------------------------------------------------------

It is recommended to create *asynchronous generators* only after the
event loop has been created.

To ensure that asynchronous generators close reliably, the event loop
uses the "sys.set_asyncgen_hooks()" function to register callback
functions. These callbacks update the list of running asynchronous
generators to keep it in a consistent state.

When the "loop.shutdown_asyncgens()" function is called, the running
generators are stopped gracefully and the list is cleared.

The asynchronous generator invokes the corresponding system hook
during its first iteration. At the same time, the generator records
that the hook has been called and does not call it again.

Therefore, if iteration begins before the event loop is created, the
event loop will not be able to add the generator to its list of active
generators because the hooks are set after the generator attempts to
call them. Consequently, the event loop will not be able to terminate
the generator if necessary.

次に示す例について考えてみてください。:

   import asyncio

   async def agenfn():
       try:
           yield 10
       finally:
           await asyncio.sleep(0)


   with asyncio.Runner() as runner:
       agen = agenfn()
       print(runner.run(anext(agen)))
       del agen

出力:

   10
   Exception ignored while closing generator <async_generator object agenfn at 0x000002F71CD10D70>:
   Traceback (most recent call last):
     File "example.py", line 13, in <module>
       del agen
           ^^^^
   RuntimeError: async generator ignored GeneratorExit

This example can be fixed as follows:

   import asyncio

   async def agenfn():
       try:
           yield 10
       finally:
           await asyncio.sleep(0)

   async def main():
       agen = agenfn()
       print(await anext(agen))
       del agen

   asyncio.run(main())


Avoid concurrent iteration and closure of the same generator
------------------------------------------------------------

Async generators may be reentered while another "__anext__()" /
"athrow()" / "aclose()" call is in progress. This may lead to an
inconsistent state of the async generator and can cause errors.

Let's consider the following example:

   import asyncio

   async def consumer():
       for idx in range(100):
           await asyncio.sleep(0)
           message = yield idx
           print('received', message)

   async def amain():
       agenerator = consumer()
       await agenerator.asend(None)

       fa = asyncio.create_task(agenerator.asend('A'))
       fb = asyncio.create_task(agenerator.asend('B'))
       await fa
       await fb

   asyncio.run(amain())

出力:

   received A
   Traceback (most recent call last):
     File "test.py", line 38, in <module>
       asyncio.run(amain())
       ~~~~~~~~~~~^^^^^^^^^
     File "Lib/asyncio/runners.py", line 204, in run
       return runner.run(main)
              ~~~~~~~~~~^^^^^^
     File "Lib/asyncio/runners.py", line 127, in run
       return self._loop.run_until_complete(task)
              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
     File "Lib/asyncio/base_events.py", line 719, in run_until_complete
       return future.result()
              ~~~~~~~~~~~~~^^
     File "test.py", line 36, in amain
       await fb
   RuntimeError: anext(): asynchronous generator is already running

Therefore, it is recommended to avoid using asynchronous generators in
parallel tasks or across multiple event loops.
