Что такое coroutine и как он работает в Python
Что проверяют
Понимаете ли вы, что корутина сама по себе ничего не выполняет параллельно, и кто на самом деле переключает задачи. Следом почти всегда спрашивают про разницу с потоками и про то, что будет, если внутри корутины вызвать time.sleep() или requests.get().
Как отвечать
Корутина это функция, которая умеет приостанавливаться и продолжать работу с того же места. Объявляется через async def, и её вызов не выполняет тело, а возвращает объект корутины. Запустится она, только когда её дождутся через await или отдадут циклу событий через asyncio.create_task(). Иначе будет предупреждение coroutine ... was never awaited.
Как это работает:
- цикл событий: один поток, в котором asyncio по очереди продвигает задачи;
- точки переключения: управление отдаётся только на
awaitтого, что реально ждёт, например сокета или таймера. Это кооперативная многозадачность, в отличие от потоков, которые ОС переключает когда угодно; - устройство: корутины выросли из генераторов и так же поддерживают
send(),throw()иclose(). Старый стиль@asyncio.coroutineсyield fromудалён в Python 3.11; - конкурентность: несколько операций запускают через
asyncio.gather()илиasyncio.TaskGroup(3.11+), ограничение по времени задают черезasyncio.timeout().
Выигрыш корутины дают на вводе-выводе: тысячи сетевых запросов обслуживаются в одном потоке без затрат на переключение потоков ОС. Для вычислений они бесполезны: CPU-задачу отправляют в ProcessPoolExecutor, а блокирующую библиотеку в asyncio.to_thread().
Где спотыкаются
Вызывают блокирующий код внутри async def. time.sleep(), requests или синхронный драйвер базы останавливают весь цикл, и остальные задачи ждут. Нужны asyncio.sleep(), httpx или aiohttp, asyncpg.
Создают задачу через create_task() и не сохраняют ссылку. Цикл держит на задачи только слабые ссылки, и незавершённую задачу может забрать сборщик мусора, об этом прямо сказано в документации.
Глушат asyncio.CancelledError. С 3.8 это наследник BaseException, и except Exception его не ловит, но голый except: без повторного raise ломает отмену задач и работу asyncio.timeout().