8.8. 锁#
asyncio.Lock 在协程之间提供互斥——保证同一时刻只有一个协程持有该锁,其他协程则等待,直到持有者释放它。
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 顺序得到服务。没有优先级,没有可重入性(同一个任务不能获取它已经持有的锁),获取操作也没有超时。要给锁的获取加上截止时间,请用 asyncio.wait_for() 包裹该获取操作:
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()