В сервисе нужно ограничить число корутин, одновременно обращающихся к внешнему API. Что именно делает asyncio.Semaphore с корутинами, превысившими лимит?
asyncio.Semaphore хранит счётчик доступных разрешений. Если разрешений нет, корутина приостанавливается на операции захвата и возобновляется после освобождения разрешения другой корутиной; поток event loop при этом не блокируется.
Ограничитель типа semaphore появился как общий примитив управления доступом к ограниченному ресурсу: соединениям, файлам, слотам оборудования или внешнему сервису. В асинхронной модели тот же подход нужен, чтобы большое число задач не создавало чрезмерную нагрузку на ресурс.
asyncio.Semaphore адаптирует этот примитив к кооперативной работе корутин. Вместо блокировки потока он переводит ожидающую корутину в состояние ожидания и позволяет event loop выполнять другие задачи.
Запуск множества корутин не означает, что внешняя система сможет принять такое же число одновременных запросов. Без ограничения можно получить исчерпание соединений, ответы с ошибками ограничения частоты, рост задержек и перегрузку самого приложения.
Семафор ограничивает именно число корутин, прошедших участок захвата разрешения. Он не ограничивает количество созданных задач и не делает синхронный код внутри корутины неблокирующим.
Семафор создаётся с начальным числом разрешений. При успешном захвате счётчик уменьшается; при освобождении увеличивается. Если счётчик равен нулю, вызов acquire() ожидает, не занимая поток event loop.
Минимальный пример:
В этом примере одновременно выполняется не более двух участков внутри async with. После выхода из блока разрешение освобождается автоматически, в том числе при штатном исключении; это снижает риск утечки разрешения по сравнению с ручными вызовами acquire() и release().
Ожидающие корутины возобновляются, когда появляется свободное разрешение. Точный порядок ожидания не следует использовать как механизм приоритизации, если он явно не гарантирован контрактом используемого примитива.
Семафор не защищает автоматически общее состояние от логических ошибок и не заменяет Lock, если нужен эксклюзивный доступ. Также он не прерывает уже выполняющуюся операцию: если лимит равен двум, уменьшение лимита не остановит две уже допущенные корутины.
Если требуется обнаруживать несбалансированное увеличение числа разрешений, применяют asyncio.BoundedSemaphore. Обычный семафор допускает увеличение счётчика после release(), поэтому ошибочный лишний release может остаться незамеченным.
Сервис запускает сотни задач для обращения к API, которое разрешает только десять одновременных запросов. Вариант без ограничения прост, но приводит к пикам нагрузки и ответам о превышении лимита. Ограничение числа задач вручную через разбиение списка снижает нагрузку, но плохо подходит для постоянно поступающих работ.
Выбран asyncio.Semaphore(10) вокруг участка сетевого запроса. Такой вариант сохраняет все задачи в общей очереди, не блокирует event loop и ограничивает только критическую операцию. Тайм-аут запроса и обработку отмены всё равно нужно реализовать отдельно: семафор не защищает от бесконечного ожидания внешнего API.
Освобождается ли разрешение, если корутина отменена внутри защищённого блока?
При использовании async with semaphore контекстный менеджер выполняет освобождение при выходе из блока, в том числе при отмене или исключении после успешного захвата. При ручном управлении нужно гарантировать release() через finally; иначе часть ёмкости семафора будет потеряна, и новые корутины могут ждать бесконечно.
Ограничивает ли семафор число HTTP-запросов, если запрос выполняется в отдельном потоке?
Да, если захват семафора охватывает запуск и ожидание этой операции. Семафор ограничивает число корутин, которым разрешён проход через участок, независимо от того, выполняется работа в event loop или через потоковый исполнитель. Однако он не ограничивает другие места программы, которые используют тот же ресурс без этого семафора.
Что произойдёт, если корутина ожидает семафор и будет отменена?
Она перестанет ждать и получит CancelledError; неиспользованное разрешение за неё освобождать не нужно, поскольку захват не состоялся. Код, управляющий группой задач, должен корректно обработать отмену, а уже захваченные разрешения следует освобождать через async with или finally.