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