Программирование PythonКонкурентность и asyncioPython-разработчик серверных приложений

Сохраняет ли asyncio.Lock порядок ожидания корутин при освобождении блокировки?

Сохраняет ли asyncio.Lock порядок ожидания корутин при освобождении блокировки?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Да. asyncio.Lock предоставляет справедливое FIFO-ожидание: если несколько корутин уже ждут захвата одной блокировки, первой продолжит корутина, которая начала ждать раньше остальных. Это относится именно к очереди ожидающих захват, а не ко всем задачам event loop.

Гарантия не означает, что корутина немедленно получит процессор после освобождения блокировки: между пробуждением и фактическим продолжением event loop может обработать другие готовые callbacks или задачи.

Исторический контекст

Асинхронные программы выполняются конкурентно даже в одном потоке: корутина добровольно уступает управление на await, после чего другая корутина может изменить общее состояние. Поэтому asyncio понадобились асинхронные примитивы синхронизации, не блокирующие поток event loop.

Справедливый порядок уменьшает риск голодания: корутина, которая уже ждёт ресурс, не должна бесконечно проигрывать новым конкурентам. Это отличается от произвольного планирования обычных потоков операционной системой.

Постановка проблемы

Без упорядоченного ожидания одна из корутин может регулярно терять блокировку более быстрым или чаще планируемым конкурентам. Для очередей запросов, последовательного доступа к кэшу или дозированной обработки ресурсов это приводит к непредсказуемым задержкам.

При этом asyncio.Lock не превращает критическую секцию в атомарную относительно потоков и процессов. Он защищает только корутины, использующие тот же экземпляр блокировки в том же event loop.

Подробное решение

При вызове acquire() свободная блокировка захватывается сразу. Если она занята, корутина добавляется в очередь ожидающих и приостанавливается; event loop при этом продолжает выполнять другие задачи.

После release() разблокированной становится следующая ожидающая корутина в порядке постановки в очередь. Отмена ожидающей корутины удаляет её из эффективной очереди, поэтому отменённый участник не блокирует остальных.

Пример показывает порядок двух корутин, которые гарантированно встают в очередь до освобождения блокировки:

import asyncio async def main(): lock = asyncio.Lock() await lock.acquire() async def worker(name): async with lock: print(name) first = asyncio.create_task(worker("A")) await asyncio.sleep(0) second = asyncio.create_task(worker("B")) await asyncio.sleep(0) lock.release() await asyncio.gather(first, second) asyncio.run(main())

Здесь A начнёт выполнение раньше B, потому что первой встала в очередь ожидания. Использовать asyncio.Lock для защиты кода, который выполняется в разных потоках, нельзя: примитив не является потокобезопасным. Для межпоточной или межпроцессной синхронизации нужны другие механизмы.

Ситуация из практики

В сервисе несколько корутин обновляют общий асинхронный кэш. Вариант без блокировки допускает потерю обновлений. Semaphore ограничил бы число одновременных операций, но не выразил бы требование единоличного доступа; последовательная очередь вручную была бы сложнее и могла бы неправильно обрабатывать отмену.

Выбран asyncio.Lock вокруг короткой операции чтения и обновления. Он сохранил порядок уже ожидающих корутин и не блокировал event loop. Важно было не помещать внутрь блокировки длительный внешний запрос: иначе корутины ждали бы не из-за защиты состояния, а из-за неоправданно длинной критической секции.

Что кандидаты часто упускают

  1. Означает ли FIFO-гарантия, что корутины получают управление строго в порядке создания задач?

Нет. Гарантия касается только корутин, которые уже ожидают acquire() на конкретном занятом lock. Порядок создания задач, порядок достижения точки захвата и порядок готовности event loop могут различаться. Корутина, которая ещё не дошла до acquire, не считается участником очереди ожидания.

  1. Что произойдёт с порядком, если ожидающую захват корутину отменить?

Отменённая корутина перестаёт быть действующим ожидающим. После освобождения блокировки её пропускают, и следующая оставшаяся корутина получает возможность захватить lock. Поэтому отмену нужно корректно распространять: если корутина уже вошла в async with lock, выход из контекстного менеджера освободит блокировку даже при отмене внутри критической секции.

  1. Можно ли безопасно захватить один asyncio.Lock дважды в рамках одной корутины?

Нет. asyncio.Lock не является реентерабельным. Второй acquire() той же корутины будет ждать освобождения блокировки, но освободить её она сможет только после выхода из второго ожидания, что создаёт самоблокировку. Если нужна вложенная логика, критические секции следует перестроить или передавать уже захваченный ресурс явно.