asyncio로 개발하기
******************

비동기 프로그래밍은 고전적인 "순차적" 프로그래밍과 다릅니다.

이 페이지는 흔한 실수와 함정을 나열하고, 이를 피하는 방법을 설명합니다
.


디버그 모드
===========

기본적으로 asyncio는 프로덕션 모드로 실행됩니다. 개발을 쉽게 하려고
asyncio에는 *디버그 모드*를 제공합니다.

여러 가지 방법으로 asyncio 디버그 모드를 활성화할 수 있습니다:

* "PYTHONASYNCIODEBUG" 환경 변수를 "1"로 설정.

* 파이썬 개발 모드 사용.

* "debug=True"를 "asyncio.run()"로 전달.

* "loop.set_debug()"를 호출.

디버그 모드를 활성화하는 것 외에도, 다음을 고려하십시오:

* asyncio 로거의 로그 수준을 "logging.DEBUG"로 설정, 예를 들어 응용 프
  로그램 시작 시 다음 코드 조각을 실행할 수 있습니다:

     logging.basicConfig(level=logging.DEBUG)

* "ResourceWarning" 경고를 표시하도록 "warnings" 모듈을 구성. 이렇게
  하는 한 가지 방법은 "-W" "default" 명령 줄 옵션을 사용하는 것입니다.

디버그 모드가 활성화되면:

* 많은 스레드 안전하지 않은 asyncio API("loop.call_soon()"과
  "loop.call_at()" 메서드와 같은)가 잘못된 스레드에서 호출될 때 예외를
  발생시킵니다.

* I/O 선택기의 실행 시간은 I/O 연산 수행에 너무 오래 걸리면 로그 됩니
  다.

* 100 밀리 초 보다 오래 걸리는 콜백이 로그 됩니다.
  "loop.slow_callback_duration" 어트리뷰트는 "느린" 것으로 간주할 최소
  실행 시간(초)을 설정하는 데 사용될 수 있습니다.


동시성과 다중 스레드
====================

이벤트 루프는 스레드(일반적으로 주 스레드)에서 실행되며 그 스레드에서
모든 콜백과 태스크를 실행합니다. 태스크가 이벤트 루프에서 실행되는 동
안, 다른 태스크는 같은 스레드에서 실행될 수 없습니다. 태스크가 "await"
표현식을 실행하면, 실행 중인 태스크가 일시 중지되고 이벤트 루프는 다음
태스크를 실행합니다.

다른 OS 스레드에서 *콜백*을 예약하려면, "loop.call_soon_threadsafe()"
메서드를 사용해야 합니다. 예:

   loop.call_soon_threadsafe(callback, *args)

거의 모든 asyncio 객체는 스레드 안전하지 않습니다. 태스크나 콜백 외부
에서 작동하는 코드가 없으면 일반적으로 문제가 되지 않습니다. 그러한 코
드가 저수준 asyncio API를 호출해야 하면, "loop.call_soon_threadsafe()"
메서드를 사용해야 합니다, 예를 들어:

   loop.call_soon_threadsafe(fut.cancel)

다른 OS 스레드에서 코루틴 객체를 예약하려면,
"run_coroutine_threadsafe()" 함수를 사용해야 합니다. 결과에 액세스할
수 있도록 "concurrent.futures.Future"를 반환합니다:

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

   # 나중에 다른 OS 스레드에서:

   future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
   # 결과를 기다립니다:
   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초 동안 수행하면, 모든 동시 asyncio 태스크와 IO
연산이 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)

Network logging can block the event loop. It is recommended to use a
separate thread for handling logs or use non-blocking IO. For example,
see 블록 하는 처리기 다루기.


await 하지 않은 코루틴 감지
===========================

코루틴 함수가 호출되었지만 기다리지 않을 때(예를 들어, "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 객체가 await 되지 않으
면, 예외는 절대로 사용자 코드로 전파되지 않습니다. 이럴 때, Future 객
체가 가비지 수집될 때 asyncio가 로그 메시지를 출력합니다.

처리되지 않은 예외의 예:

   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) # immitate 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.

Consider the following example:

   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.
