Каков принцип асинхронного подхода
Что проверяют
Отличаете ли вы конкурентность от параллелизма. Интервьюер хочет услышать, что asyncio работает в одном потоке и выигрывает только там, где код ждёт ввод-вывод. Следом обычно спрашивают, что будет, если внутри корутины вызвать time.sleep или requests.get.
Как отвечать
Суть: пока одна задача ждёт ответа сети или диска, поток не простаивает, а переключается на другую. Переключение кооперативное, задача сама отдаёт управление в точке await.
Каркас ответа:
- event loop: один цикл в одном потоке, который следит за готовностью сокетов и таймеров и возобновляет задачи, дождавшиеся своего события;
- корутина: функция
async def. Её вызов ничего не выполняет, а создаёт объект корутины, который нужно запустить черезawait,asyncio.runили обернуть в задачу; - задачи:
asyncio.create_task,asyncio.gatherилиasyncio.TaskGroup(Python 3.11+) запускают корутины конкурентно. Последовательныеawaitподряд ничего не ускоряют: вторая операция начнётся, когда закончится первая; - блокирующий код: синхронный вызов останавливает весь цикл. Его выносят в
asyncio.to_threadилиrun_in_executor, а для сети и базы берут асинхронные клиенты:httpx,aiohttp,asyncpg; - CPU-задачи: асинхронность их не ускоряет. Для вычислений нужны процессы (
ProcessPoolExecutor) или библиотеки, которые отпускают GIL.
Хорошо привязать к практике: в FastAPI обработчик async def выполняется прямо в цикле, а обычный def уходит в пул потоков, поэтому синхронный драйвер базы внутри async def роняет пропускную способность всего сервиса.
Где спотыкаются
Называют асинхронность параллельным выполнением. В одном цикле asyncio в каждый момент работает ровно один фрагмент Python-кода.
Пишут await a() и следом await b() и ждут ускорения. Чтобы ожидания шли одновременно, задачи нужно запустить вместе, через gather или TaskGroup.
Создают задачу через create_task и не сохраняют ссылку на неё. Цикл держит на задачи только слабые ссылки, и незавершённую задачу может собрать сборщик мусора, об этом прямо предупреждает документация.