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 慣用法

The example above shows the recommended way to use a lock: inside an async with block. On entry the block awaits acquire(); on exit (whether the block returned normally, raised an exception, or was cancelled) the lock is released automatically. There is no path out of the block that leaves the lock held.

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

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()