asyncio и event loop на собеседовании

Что произойдёт, если внутри корутины вызвать блокирующий код: документация отвечает числом. Разбираем кооперативность цикла и цену одной ошибки.

Асинхронный код можно писать раньше, чем понимаешь его устройство, до тех пор, пока не понадобится объяснить, что произойдёт, если внутри корутины вызвать обычный блокирующий код. Документация отвечает на это числом, и из её ответа выводится вся модель целиком. Ниже она и разобрана: от того, как цикл переключает задачи, до границы с обычными потоками.

Всё, что сказано о поведении, относится к Python 3.

Одна задача за раз §

Модель цикла событий документация описывает коротко: цикл работает в потоке, как правило главном, и выполняет все обратные вызовы и задачи в этом потоке. Пока задача выполняется, никакая другая задача в том же потоке выполняться не может.

Переключение происходит в одной-единственной точке: когда задача доходит до await, она приостанавливается, и цикл выполняет следующую. Документация называет это кооперативным планированием: цикл выполняет одну задачу за раз, и пока задача ожидает завершения другого объекта, цикл занимается остальными задачами, обратными вызовами или вводом-выводом.

Из этого следует различение, которое стоит проговорить вслух, потому что на нём построена вся тема: asyncio даёт конкурентность, а не параллелизм. Задачи чередуются в одном потоке. Слово «кооперативное» здесь точное: задача отдаёт управление сама, и только в точке await.

Что будет при блокирующем вызове §

Это центральный пункт темы, и документация формулирует его настолько конкретно, что ответ стоит запомнить близко к тексту. Блокирующий код (документация уточняет: CPU-bound) вызывать напрямую нельзя. Причина сформулирована примером: если функция выполняет вычисление в течение одной секунды, все конкурентные задачи asyncio и все операции ввода-вывода задержатся на одну секунду.

Механизм понятен из предыдущей секции. Переключение возможно только на await; блокирующий вызов до await не доходит, значит, управление не возвращается циклу вовсе. Задержится весь цикл, включая обслуживание сети.

Средство названо там же: увести такую работу в исполнитель через loop.run_in_executor(), в другой поток, в другой интерпретатор или даже в другой процесс, чтобы не блокировать поток с циклом. Для функций, которые блокируются на вводе-выводе, есть более прямое средство, asyncio.to_thread: документация относит его именно к I/O-bound функциям, которые иначе заблокировали бы цикл.

Полезно назвать и способ обнаружения, он превращает теорию в проверяемое: в отладочном режиме asyncio логирует обратные вызовы, выполняющиеся дольше 100 миллисекунд, а порог настраивается через loop.slow_callback_duration.

Корутина сама не запускается §

Различие между корутиной и задачей выглядит терминологическим ровно до момента, когда код молча ничего не делает. Документация формулирует это прямо: простой вызов корутинной функции не планирует её выполнение. Вызов возвращает объект корутины, и всё; если его никто не ожидает, корутина не выполнится вообще, а интерпретатор выдаст предупреждение о том, что её так и не дождались.

Задача — это обёртка, добавляющая недостающее: корутину оборачивают в задачу и планируют её выполнение. С этого момента она попадает в очередь цикла и выполнится сама, без явного ожидания в точке создания.

Отсюда выводится и назначение gather: он выполняет переданные объекты конкурентно и возвращает список результатов в том порядке, в котором были переданы аргументы, независимо от порядка завершения. Последнее уточнение стоит помнить: порядок результатов детерминирован, порядок выполнения — нет.

Практический вывод, который связывает секцию с предыдущей: последовательность из двух await даёт два ожидания подряд. Конкурентность появляется тогда, когда обе операции запланированы до того, как начали ожидать результат первой.

asyncio и потоки §

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

Оговорка, которую документация делает следом, объясняет, почему это редко замечают: проблемой это обычно не становится, пока нет кода, работающего с этими объектами вне задачи или обратного вызова. Внутри цикла всё согласовано по построению, опасность появляется на границе с обычными потоками.

Правило для этой границы названо одно: чтобы запланировать обратный вызов из другого потока операционной системы, используется loop.call_soon_threadsafe(). Это тот случай, когда знание конкретного имени метода уместно: оно и есть ответ.

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

Проверьте себя

Сначала проговорите ответ, потом сверьтесь с текстом.

Как работает event loop?

Что проверяют: знаете вы, где именно происходит переключение, или описываете цикл как «выполняет задачи асинхронно».

Частая ошибка: сказать, что цикл выполняет задачи параллельно. Он выполняет их по очереди в одном потоке; одновременности здесь нет.

Раздел «Одна задача за раз»
Что произойдёт, если внутри корутины вызвать блокирующий код?

Что проверяют: понимаете вы, что задержка распространяется на весь цикл.

Частая ошибка: ответить «эта корутина будет выполняться дольше». Дольше будут выполняться все задачи и все операции ввода-вывода, ровно на длительность блокирующего вызова.

Раздел «Что будет при блокирующем вызове»
Чем корутина отличается от задачи?

Что проверяют: знаете вы, что планирование — отдельное действие, или считаете, что async def уже делает функцию выполняющейся конкурентно.

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

Раздел «Корутина сама не запускается»
Можно ли обращаться к asyncio-объектам из другого потока?

Что проверяют: знаете вы про ограничение потокобезопасности и про предусмотренный для него способ.

Частая ошибка: ответить «да, всё равно ведь один процесс». Почти все объекты asyncio непотокобезопасны; для планирования из чужого потока есть отдельный метод.

Раздел «asyncio и потоки»

Проверьте эти темы вслух

В тренажёре вы отвечаете голосом, а разбор показывает, где ответ просел.