8.8. Блокування¶
asyncio.Lock забезпечує взаємне виключення між корутинами – гарантію, що лише одна корутина одночасно утримує блокування, а інші чекають, поки власник не звільнить його.
8.8.1. Коли потрібне блокування¶
Сторінка кооперативного паралелізму зазначила, що дві корутини не можуть чергуватися посередині коду, в якому немає await. Протилежне також вірно: щойно корутина виконує awaits, цикл може запустити іншу корутину. Якщо дві корутини звертаються до одного ресурсу між 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. Немає пріоритету, немає реентерабельності (одне й те саме завдання не може повторно захопити вже утримуване ним блокування) і немає тайм-ауту на захоплення. Щоб встановити дедлайн на захоплення блокування, загорніть acquire у 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()