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 возвращает управление в цикл из чужого потока. Два разных направления одной границы, и путать их — типичный источник ошибок.

Backend-разработчик Python
Учебный маршрут backend-разработчика на Python: язык и среда выполнения CPython, конкурентность, веб-фреймворки и API, данные и ORM, безопасность, продакшн-инженерия и архитектура.

Backend Developer Python
Глубокое понимание Python и его среды выполнения для backend-разработки. Курс разбирает не синтаксис, а семантику языка и поведение CPython: объектную модель и работу со ссылками, модель данных и протоколы, конкурентность на asyncio и следствия глобальной блокировки интерпретатора, прикладной веб-слой (FastAPI, Django, Flask), работу с данными через ORM и продакшн-инженерию. Цель — научиться видеть стоимость абстракций и поведение кода под нагрузкой, а не просто заставлять его работать.
Готовьтесь не вслепую
Возьмите маршрут по своей роли и отрабатывайте ответы на вопросы с AI-разбором.