Что происходит с выполнением других задач asyncio, когда корутина достигает await операции ввода-вывода?
Корутина приостанавливается на await, если ожидаемая операция не готова немедленно, и управление возвращается циклу событий. Цикл событий в это время запускает другие готовые задачи, поэтому один поток может обслуживать множество операций ввода-вывода без отдельного потока на каждую задачу.
Это работает только для операций, которые действительно передают управление циклу событий. Синхронный блокирующий вызов внутри корутины остановит весь цикл и не позволит другим задачам выполняться.
Асинхронный ввод-вывод появился как способ обслуживать большое число сетевых соединений без создания отдельного потока или процесса для каждого соединения. Такой подход снижает накладные расходы на переключение контекста и потребление памяти, но требует кооперативной модели выполнения.
В Python библиотека asyncio стала стандартным инструментом для этой модели: цикл событий отслеживает готовность операций, а корутины добровольно уступают управление в точках ожидания. Синтаксис async и await сделал такой код более читаемым по сравнению с ручным управлением обратными вызовами.
Сетевая операция часто большую часть времени ждёт данные, а не использует процессор. Если выполнять её синхронно, поток простаивает, хотя процесс мог бы обрабатывать другие запросы.
Неверное понимание механизма приводит к двум типичным последствиям. Запуск большого числа корутин не ускоряет CPU-bound вычисления, а блокирующий вызов внутри одной корутины может заморозить все остальные задачи этого цикла событий.
asyncio использует кооперативную многозадачность. Корутина выполняется до тех пор, пока не завершится, не встретит точку ожидания, которая ещё не готова, или явно не передаст управление.
При await корутина не занимает поток постоянным ожиданием. Цикл событий регистрирует интерес к завершению операции и выбирает другую готовую задачу. Когда операционная система сообщает о готовности ввода-вывода, цикл событий возобновляет соответствующую корутину с места остановки.
Минимальная иллюстрация передачи управления:
asyncio.sleep(0) здесь используется как явная точка уступки управления; это не имитация полезной работы и не способ ускорить вычисления. В реальном приложении такой точкой обычно является асинхронная операция сокета, таймер или ожидание другой задачи.
GIL не является механизмом планирования корутин. Он ограничивает одновременное выполнение Python-байткода в нескольких потоках конкретной реализацией CPython, тогда как asyncio переключает корутины в основном потоке в заранее определённых точках ожидания. Поэтому asyncio хорошо подходит для большого числа I/O-bound задач, но не делает CPU-bound вычисления параллельными.
Для блокирующей функции используют отдельный поток или процесс через подходящий исполнитель, либо заменяют библиотеку на асинхронную. Поток позволяет не блокировать цикл событий, но не устраняет ограничения GIL для чистого Python-кода; процесс даёт отдельное адресное пространство и может использовать несколько ядер, однако требует затрат на обмен данными и управление процессами.
Сервис на asyncio принимает HTTP-запросы и внутри обработчика вызывает старую синхронную библиотеку, которая ждёт ответ внешней системы. При высокой нагрузке задержка одного внешнего вызова блокирует поток цикла событий, поэтому растёт время ответа всех запросов, даже тех, которым внешняя система не нужна.
Рассматриваются три варианта. Оставить вызов синхронным проще всего, но это блокирует весь цикл. Перенести вызов в процесс устраняет блокировку и изолирует CPU-нагрузку, однако добавляет стоимость межпроцессного обмена. Перенести вызов в отдельный поток обычно дешевле для преимущественно ожидающей блокирующей операции, но требует контроля числа потоков и безопасного использования библиотеки.
Практичный выбор — выполнять старую блокирующую функцию вне цикла событий в ограниченном пуле потоков, а для новых интеграций использовать асинхронный клиент. Это сохраняет отзывчивость цикла, ограничивает рост ресурсов и не требует переписывать всю систему сразу. При этом отдельно контролируют тайм-ауты, отмену задач и насыщение пула.
Вопрос: Всегда ли await передаёт управление другой задаче?
Ответ: Нет. Если ожидаемый объект уже завершён или операция может закончиться немедленно, переключение на другую задачу может не произойти. Кроме того, сам await не превращает синхронную блокирующую функцию в неблокирующую: если внутри выполняется обычное ожидание, цикл событий остаётся заблокированным.
Вопрос: Почему большое число корутин не ускоряет вычисление хешей или обработку изображений?
Ответ: Такие операции используют процессор, а не ждут внешний ввод-вывод. В одном цикле событий корутины не выполняются параллельно, а частые переключения между ними добавляют накладные расходы. Для CPU-bound работы обычно рассматривают процессы или специализированные библиотеки, освобождающие интерпретатор и использующие нативный параллелизм.
Вопрос: Что произойдёт с корутиной, если ожидаемая задача будет отменена?
Ответ: При распространении отмены ожидание обычно завершается исключением CancelledError в ожидающей корутине. Код корутины может перехватить отмену для освобождения ресурсов, но после очистки должен корректно завершить отмену, если нет осознанной причины подавлять её. Игнорирование отмены может привести к зависшим задачам, утечкам ресурсов и некорректному завершению приложения.