8.8. 鎖(Locks)#

asyncio.Lock 在協程之間提供 互斥(mutual exclusion) ——保證同一時間只有一個協程持有該鎖,其餘協程則等待持有者釋放它。

8.8.1. 何時需要鎖#

協作式並行 頁面指出,兩個協程無法在一段不含 await 的程式碼中途交錯執行。反過來 也成立:只要某個協程執行 await,事件迴圈便可自由執行另一個協程。如果兩個協程跨越多個 await 存取同一項資源——例如 UART、I2C 或 SPI 匯流排——它們的操作便可能以破壞該資源的方式交錯。

在臨界區段周圍加上鎖即可封閉這個破口::

import asyncio

bus_lock = asyncio.Lock()

async def read_register(bus, addr):
    async with bus_lock:
        bus.write(addr)
        return await bus.read(2)

現在兩個協程都可以並行呼叫 read_register;鎖確保同一時間只有其中一個持有匯流排,另一個則須等待鎖被釋放後才能開始。

當臨界區段內部沒有 await 時, 需要鎖——協作式排程的保證已涵蓋這種情況。只有當臨界區段在執行途中將控制權交還給事件迴圈時,才需要鎖。

8.8.2. async with 慣用法#

上面的範例展示了使用鎖的建議方式:放在 async with 區塊內。進入區塊時會 await acquire();離開區塊時(無論是正常返回、拋出例外,還是被取消),鎖都會自動釋放。沒有任何離開區塊的路徑會使鎖維持在被持有的狀態。

對於鎖的存活期與某段程式碼區塊無法對齊的少見情況,這些方法也可直接使用::

await bus_lock.acquire()
try:
    bus.write(addr)
    result = await bus.read(2)
finally:
    bus_lock.release()

需要 try/finally 才能使其等同於 async with 版本。async with 形式之所以存在,正是因為 才是正確的寫法,而語言本身讓它變得簡潔。

8.8.3. 方法參考#

  • acquire() ——一個協程。會阻塞直到鎖解開,然後取得它。

  • release() ——釋放鎖。如果有任何協程正排隊等待 acquire(),佇列中的下一個會被排程執行,而鎖維持鎖定狀態;否則鎖會變為解開狀態。

  • locked() ——若鎖目前被持有則回傳 True,否則回傳 False。立即回傳,不會阻塞。

等待者依 FIFO 順序受理。沒有優先權、沒有可重入性(同一個任務無法再取得它已持有的鎖),acquire 也沒有逾時。若要為取得鎖設定期限,可用 asyncio.wait_for() 包覆 acquire::

try:
    await asyncio.wait_for(bus_lock.acquire(), timeout=1)
except asyncio.TimeoutError:
    # lock busy for >1 s -- bail out
    return None
try:
    ...
finally:
    bus_lock.release()