使用 asyncio 開發
*****************

非同步程式設計 (asynchronous programming) 與傳統的"順序"程式設計
(sequential programming) 不同。

本頁列出常見的錯誤和陷阱，並解釋如何避免它們。


除錯模式
========

在預設情況下 asyncio 以正式生產模式 (production mode) 執行。為了讓開發
更輕鬆，asyncio 還有一種*除錯模式 (debug mode)*。

有幾種方法可以啟用 asyncio 除錯模式：

* 將 "PYTHONASYNCIODEBUG" 環境變數設定為 "1"。

* 使用 Python 開發模式 (Development Mode)。

* 將 "debug=True" 傳遞給 "asyncio.run()"。

* 呼叫 "loop.set_debug()"。

除了啟用除錯模式外，還要考慮：

* 將 asyncio logger（日誌記錄器）的日誌級别設置為 "logging.DEBUG"，例
  如下面的程式片段可以在應用程式啟動時運行：

     logging.basicConfig(level=logging.DEBUG)

* 配置 "warnings" 模組以顯示 "ResourceWarning" 警告。一種方法是使用
  "-W" "default" 命令列選項。

啟用除錯模式時：

* 許多非執行緒安全 (non-threadsafe) 的 asyncio APIs（例如
  "loop.call_soon()" 和 "loop.call_at()" 方法），如果從錯誤的執行緒呼
  叫就會引發例外。

* 如果執行一個 I/O 操作花費的時間太長，則將 I/O 選擇器 (selector) 的執
  行時間記錄到日誌中。

* 執行時間超過 100 毫秒的回呼 (callback) 將會被記錄於日誌。屬性
  "loop.slow_callback_duration" 可用於設定以秒為單位的最小執行持續時間
  ，超過這個值執行時間就會被視為"緩慢"。


並行性和多執行緒 (Concurrency and Multithreading)
=================================================

事件迴圈在執行緒中運行（通常是主執行緒），並在其執行緒中執行所有回呼和
Tasks（任務）。當一個 Task 在事件迴圈中運行時，沒有其他 Task 可以在同
一個執行緒中運行。當一個 Task 執行一個 "await" 運算式時，正在執行的
Task 會被暫停，而事件迴圈會執行下一個 Task。

要從不同的 OS 執行緒為一個 *callback* 排程，應該使用
"loop.call_soon_threadsafe()" 方法。例如：

   loop.call_soon_threadsafe(callback, *args)

幾乎所有 asyncio 物件都不支援執行緒安全 (thread safe)，這通常不是問題
，除非在 Task 或回呼函式之外有程式需要和它們一起運作。如果需要這樣的程
式來呼叫低階 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)

   # Later in another OS thread:

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

為了能夠處理訊號，事件迴圈必須於主執行緒中運行。

"loop.run_in_executor()" 方法可以和
"concurrent.futures.ThreadPoolExecutor" 或 "InterpreterPoolExecutor"
一起使用，這能夠在作業系統上另一個不同的執行緒中執行阻塞程式，且避免阻
塞執行事件迴圈的執行緒。

目前沒有什麼辦法能直接從另一個行程（例如透過 "multiprocessing" 啟動的
程序）來為協程或回呼排程。事件迴圈方法小節列出了可以從 pipes（管道）讀
取並監視 file descriptor（檔案描述器）而不會阻塞事件迴圈的 API。此外，
asyncio 的子行程 API 提供了一種啟動行程並從事件迴圈與其通訊的辦法。最
後，之前提到的 "loop.run_in_executor()" 方法也可和
"concurrent.futures.ProcessPoolExecutor" 搭配使用，以在另一個行程中執
行程式。


執行阻塞的程式
==============

不應該直接呼叫阻塞（CPU 密集型）程式。例如一個執行 1 秒 CPU 密集型計算
的函式，那麼所有並行非同步 Tasks 和 IO 操作都會被延遲 1 秒。

一個 executor（執行器）可以被用來在不同的執行緒（包含不同直譯器）、或
甚至不同的行程中執行任務，以避免事件迴圈阻塞了 OS 執行緒。詳情請見
"loop.run_in_executor()" 方法。


日誌記錄
========

asyncio 使用 "logging" 模組，所有日誌記錄都是透過 ""asyncio"" logger
執行的。

日誌級別被預設為 "logging.INFO"，它可以很容易地被調整：

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

網路日誌記錄可能會阻塞事件迴圈。建議使用獨立的執行緒來處理日誌或使用非
阻塞 IO，範例請參見 Dealing with handlers that block。


偵測從未被等待的 (never-awaited) 協程
=====================================

當協程函式被呼叫而不是被等待時（即執行 "coro()" 而不是 "await 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()

常用的修復方法是去等待協程或者呼叫 "asyncio.create_task()" 函式：

   async def main():
       await test()


偵測從未被取得的 (never-retrieved) 例外
=======================================

如果呼叫 "Future.set_exception()"，但 Future 物件從未被等待，例外將無
法被傳播 (propagate) 到使用者程式。在這種情況下，當 Future 物件被垃圾
回收 (garbage collected) 時，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

啟用除錯模式以取得任務建立處的追蹤資訊 (traceback)：

   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
