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