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