11.8. Locks¶
asyncio.Lock provides mutual exclusion between
coroutines – a guarantee that only one coroutine at a time
holds the lock, and the others wait until the holder
releases it.
11.8.1. When a lock is needed¶
The cooperative concurrency page noted that two
coroutines cannot interleave halfway through code that has
no await in it. The opposite is also true: as soon as
a coroutine awaits, the loop is free to run another
coroutine. If two coroutines touch the same resource across
awaits – a UART, I2C, or SPI bus – their operations can
interleave in ways that corrupt the resource.
A lock around the critical section closes the gap:
import asyncio
bus_lock = asyncio.Lock()
async def read_register(bus, addr):
async with bus_lock:
bus.write(addr)
return await bus.read(2)
Two coroutines can now both call read_register
concurrently; the lock makes sure only one of them holds the
bus at a time, and the other one waits for the lock to be
released before starting.
Locks are not needed when the critical section has no
await inside it – the cooperative scheduling guarantee
already covers that case. They are only needed when the
critical section yields to the loop partway through.
11.8.2. The async with idiom¶
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.
For the rare case where the lifetime of the lock does not line up with a block of code, the methods are also available directly:
await bus_lock.acquire()
try:
bus.write(addr)
result = await bus.read(2)
finally:
bus_lock.release()
The try/finally is required to make this equivalent
to the async with version. The async with form
exists because this is the right shape and the language
makes it concise.
11.8.3. Method reference¶
acquire()– a coroutine. Blocks until the lock is unlocked, then takes it.release()– releases the lock. If any coroutines are queued waiting onacquire(), the next one in the queue is scheduled to run and the lock stays locked; otherwise the lock becomes unlocked.locked()– returnsTrueif the lock is currently held,Falseotherwise. Returns immediately; does not block.
Waiters are served in FIFO order. There is no priority, no
reentrancy (the same task cannot acquire a lock it already
holds), and no timeout on acquire. To put a deadline on a
lock acquisition, wrap the acquire in
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()