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

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 顺序得到服务。没有优先级,没有可重入性(同一个任务不能获取它已经持有的锁),获取操作也没有超时。要给锁的获取加上截止时间,请用 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()