2.32. 協程¶
協程(coroutine)是一種可以執行到中途暫停、稍後再從上次停下的地方繼續執行的函式,而且所有區域變數都保持完好。它與產生器(generator)的模式相呼應 -- 同樣是一個可以被暫停與恢復的函式主體 -- 差別只在於由誰來驅動每一次的恢復。
Python 撰寫協程的語法是 async / await 這對關鍵字。async 將函式標記為協程;await 則標記函式內部允許暫停的位置。
2.32.1. 定義協程¶
以 async def 定義的函式是協程函式。呼叫它並不會執行函式主體,而是傳回一個尚未開始執行的協程物件:
async def greet(name):
print("hello,", name)
coro = greet("Alice") # body NOT run yet
協程物件停留在函式的最起始處暫停著。必須有某個東西來驅動它,函式主體才會執行 -- 這個東西就是事件迴圈(event loop),一個執行階段的元件。MicroPython 在 asyncio 模組中內建了一個。現在,先把協程當成「準備就緒、等待驅動器」的狀態即可。
2.32.2. 在協程內部暫停¶
在協程內部,await 運算式會暫停執行,直到所等待的值準備就緒為止:
async def fetch_and_log():
data = await read_sensor()
print("got:", data)
當函式主體執行到 await read_sensor() 時,協程會把控制權交還給正在執行它的東西。當 read_sensor() 完成後,驅動器會在下一行恢復協程的執行,並將結果繫結到 data。
await 只有在協程內部才有效。在一般函式中使用它會造成語法錯誤。
2.32.3. 與產生器的關係¶
協程與產生器共用相同的底層機制。兩者的區別在於由誰來拉動每一次的恢復:
產生器產出值;消費者透過
next()或迭代來拉取下一個值。協程交出控制權;當所等待的操作就緒時,由事件迴圈安排恢復執行。
如果你能理解產生器的 yield 握手機制,那麼協程的握手就是同樣的概念 -- 只是由事件迴圈而非 for 迴圈來驅動。
事件迴圈是一個小型的分派器,它維護著一份正在等待某些事件(計時器、網路事件、另一個協程完成等)的協程清單。在每一次迭代中,它挑選一個等待條件已被滿足的協程,恢復它的執行直到下一個 await,接著記錄該協程現在正在等待什麼,然後轉向另一個就緒的協程。其結果是許多工作在單一執行緒上並行推進 -- 每個協程在自己的 await 點自願交出控制權,而迴圈則用其他任何就緒可推進的協程來填補這些空檔。
在底層,await 與 yield 使用的是同一個 Python 執行階段功能來暫停與恢復函式。關鍵字之所以不同,是因為圍繞它們的慣例不同:yield 把一個值交還給以 next() 拉取的消費者;await 則把控制權交給事件迴圈,由它在所等待的操作就緒時安排恢復執行。async / await 本質上是協程模式較新的語法 -- 較舊的函式庫是直接建構在產生器機制之上的協程,使用 yield from(在 迭代器與產生器 中介紹)在協程之間委派暫停。
2.32.4. 協程需要驅動器¶
協程若沒有執行階段來驅動,便是靜止不動的。定義一個協程沒有問題;但要執行它則需要事件迴圈。MicroPython 的 asyncio 模組提供了這個事件迴圈。Asyncio 章節將涵蓋如何啟動迴圈、在其上排程協程、透過鎖(lock)與事件(event)在協程之間共用狀態、處理取消與逾時,以及如何圍繞此處介紹的 async / await 關鍵字來打造一個真正的應用程式。