8.1. 協作式並行

Asyncio 的排程模型是協作式的,而非搶佔式的。這項區別是本節其餘部分所建立的最重要心智模型,因此值得在任何程式碼出現之前先釐清。

8.1.1. 搶佔式與協作式的對比

搶佔式排程器 — 也就是桌面作業系統用來讓眾多程式同時運行的那種 — 可以在任何時刻暫停當前正在執行的程式碼片段並切換至另一個。正在執行的程式碼不需要做任何特別的事;是排程器中斷它。這使得搶佔式排程非常靈活(沒有任何一段程式碼能因為自己跑得慢而讓其他程式碼挨餓),但也意味著任何共用變數都必須小心防護,因為切換可能發生在任何位置 — 甚至是在寫入某個值的中途,或讀取某個串列的途中。

協作式排程器只能在當前正在執行的片段明確交還控制權的那些點上,於各程式碼片段之間切換。在 asyncio 中,這些點是每一個 await 以及每一次對內部會讓出控制權的協程的呼叫(最常見的是 asyncio.sleep())。在兩個 await 之間,正在執行的協程獨佔 CPU。

由此衍生出兩項結果:

  • 從不 await 的協程永遠不會被暫停。 如果一個協程停留在沒有任何 await 的緊密迴圈中,它就會壟斷排程器,使其他一切都無法推進。修正方法是在迴圈中適當的位置 await asyncio.sleep_ms(0)(或其他某種等待呼叫)。

  • 共用狀態在各 await 之間是安全的。 兩個協程無法在一個沒有 await 的操作中途交錯執行。當搶佔落在多步驟更新的中途時所產生的那種損毀 — 一段程式碼讀取某個值,而另一段正在修改它的途中 — 在這裡根本不可能發生。當數個協程必須跨越 await 共用某項資源時,協程之間仍然需要協調,但這種「在一行的中途」交錯的問題並不適用。

8.1.2. 三個層次

每個 asyncio 指令碼都由相同的三個層次構成。接下來的兩頁會詳細介紹它們;以下這些是閱讀時需記在心中的標籤。

  • 協程(Coroutines) — 以 async def 宣告的函式,每一個都是一個自成一體的工作單元,會在適當之處 await。Python 概觀介紹過 async/await 關鍵字;在 asyncio 中,它們正是協程讓出控制權回到排程器的方式。

  • 任務(Tasks)asyncio.create_task() 包覆在協程外的封裝,用以將其與當前協程並行排程。應用程式通常會為長時間運行的工作建立少數幾個任務(快照迴圈、網路用戶端、UART 讀取器,……)。

  • 事件迴圈(The event loop) — 底層的引擎,負責追蹤哪些協程正在等待、哪些已準備好執行,並在每一個 await 處於各任務之間切換。應用程式不會撰寫這個迴圈;它將一個頂層協程交給 asyncio.run(),然後迴圈從那裡開始驅動一切。

當應用程式以這種方式描述時 — 也就是一小組由事件迴圈組合而成的協程 — 並行就成為程式形態的一項屬性,而不是應用程式必須一步步管理的事物。