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