Вопросы по асинхронности в Python на собеседовании
Асинхронность в Python спрашивают почти всегда, и почти всегда через GIL. Проверяют не определение, а понимание: почему async не ускоряет вычисления, что происходит при блокирующем вызове внутри корутины и когда всё-таки нужны процессы.
Что спрашивают
- +Event loop: как выполняются корутины, чем await отличается от вызова, что такое задача и когда она стартует
- +Координация: gather и wait, таймауты и отмена, ограничение параллелизма семафором
- +GIL: что именно он блокирует, почему потоки помогают на вводе-выводе и не помогают на вычислениях
- +Выбор модели: потоки, процессы или asyncio под конкретную задачу, цена переключения и передачи данных
- +Ловушки: блокирующий вызов в корутине, забытый await, необработанные исключения в задачах
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.
- Event loop и корутины14
- Потоки, процессы, asyncio14
- GIL13
- Координация (gather, семафоры)13
- Ловушки async13
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- Структуры данных в Python: list, dict, set, tuple19 вопросов
- Генераторы и итераторы в Python15 вопросов
- ООП в Python: классы, наследование, dunder-методы22 вопросов
- Декораторы, замыкания и области видимости в Python18 вопросов
- Исключения и контекстные менеджеры в Python19 вопросов
- GIL в Python: что блокирует и как обходят13 вопросов
- Event loop и корутины в asyncio14 вопросов
- Координация корутин: gather и семафоры13 вопросов
- Ловушки асинхронного кода13 вопросов
- Потоки, процессы и asyncio14 вопросов
Примеры вопросов с разбором
- Зачем нужен
asyncio.gather(*coros)?A)Выполнить корутины строго по очереди, по однойB)Ускорить CPU-счёт по разным ядрамC)Создать пул процессов и раскидать корутины по ядрамD)Запустить корутины конкурентно и собрать результатыпоказать ответ и разбор
+D)Запустить корутины конкурентно и собрать результаты// разбор: gather планирует все переданные корутины/таски в цикле и ждёт их все, возвращая список результатов в порядке аргументов (не в порядке завершения). Это базовый способ сделать N I/O-операций одновременно. Ускорения CPU-счёта не даёт — всё в одном потоке под GIL.
- Что возвращает вызов
async def-функции без await?A)Результат её выполненияB)Немедленно запускает тело функции в фоновом потокеC)None, пока не указан awaitD)Объект корутины, который ещё не запущенпоказать ответ и разбор
+D)Объект корутины, который ещё не запущен// разбор: Вызов корутинной функции создаёт объект корутины и НЕ исполняет тело. Код внутри начнёт работать, только когда корутину отдадут циклу событий — через await, asyncio.run или create_task. Забытый await — типовой баг: корутина «висит», рантайм выдаёт «coroutine was never awaited».
- Хендлер асинхронный, но под нагрузкой сервис встаёт колом:
Диагноз?async def handler(u): r = requests.get(u) return r.json()A)Синхронныйrequestsблокирует весь event loopB)requestsне умеет.json()C)Не хватаетawaitпередrequests.getD).json()тоже надо awaitпоказать ответ и разбор
+A)Синхронный `requests` блокирует весь event loop// разбор: requests.get — блокирующий вызов: он не отдаёт управление циклу, поэтому пока идёт запрос, ВСЕ корутины в этом потоке стоят. Один медленный бэкенд подвешивает весь сервис. Лечение: async-клиент (httpx.AsyncClient, aiohttp) с await либо вынос sync-вызова в run_in_executor.
- Что такое GIL в CPython?A)Жёсткий лимит на количество потоков в процессеB)Мьютекс: байткод исполняет один поток разомC)Сборщик мусора PythonD)Флаг компиляции для параллелизма
показать ответ и разбор
+B)Мьютекс: байткод исполняет один поток разом// разбор: Global Interpreter Lock — глобальный мьютекс интерпретатора CPython: в любой момент байткод Python исполняет ровно один поток. Он упрощает управление памятью (refcount) ценой того, что потоки не считают Python-код параллельно на разных ядрах. На I/O и в C-расширениях GIL отпускается.
- Чем
multiprocessingобходит GIL?A)Каждый процесс — свой интерпретатор и свой GILB)Переводит потоки в CC)Использует единый общий GIL, разделяемый всемиD)Отключает GIL в текущем процессепоказать ответ и разбор
+A)Каждый процесс — свой интерпретатор и свой GIL// разбор: multiprocessing поднимает отдельные процессы, у каждого свой интерпретатор Python и свой GIL — значит, они реально считают параллельно на разных ядрах. Плата: обмен данными идёт через пайпы и пиклинг (дороже общей памяти потоков), а старт процесса тяжелее старта потока.
await asyncio.gather(a(), b()), где a и b — поasyncio.sleep(1). Сколько ждём?A)Около 1 секундыB)Около 2 секундC)Зависит от числа ядерD)Мгновенно, sleep не блокируетпоказать ответ и разбор
+A)Около 1 секунды// разбор: Обе корутины спят на цикле одновременно: gather стартует их вместе, каждая на asyncio.sleep(1) отдаёт управление, цикл ждёт оба таймера параллельно — суммарно ~1 с, не 2. Заменить на синхронный time.sleep — цикл заблокируется, и выйдет ~2 с.
- Что делает
awaitвнутри корутины в точке ожидания?A)Запускает корутину в отдельном потокеB)Ускоряет исполнение, распределяя корутину по ядрамC)Отдаёт управление циклу, пока ждём результатD)Блокирует поток до готовности результатапоказать ответ и разбор
+C)Отдаёт управление циклу, пока ждём результат// разбор: await в точке ожидания (I/O, таймер) отдаёт управление циклу событий: пока эта корутина ждёт, цикл крутит другие. Поток не блокируется — суть кооперативной многозадачности. Без await-точек корутина монополизирует цикл, и «асинхронность» пропадает.
- Внутри корутины крутится тяжёлый Python-счёт на 500 мс. Что с сервисом?A)Цикл замирает на 500 мс — все запросы ждутB)Ничего, счёт идёт в фонеC)Другой поток подхватит счётD)asyncio распараллелит его по ядрам
показать ответ и разбор
+A)Цикл замирает на 500 мс — все запросы ждут// разбор: Корутина без await-точек монополизирует единственный поток цикла: 500 мс счёта = 500 мс, когда цикл не обслуживает никого. Латентность всех запросов подскакивает. CPU-счёт в async-сервисе выносят в пул процессов через loop.run_in_executor(ProcessPoolExecutor).
- Ускорят ли потоки чисто вычислительную (CPU-bound) задачу в CPython?A)Только вместе с asyncioB)Нет: GIL сериализует исполнение байткодаC)Да, но только для floatD)Да, ускоряют почти линейно по числу ядер
показать ответ и разбор
+B)Нет: GIL сериализует исполнение байткода// разбор: Для CPU-bound Python-кода потоки не ускоряют: GIL пускает исполнять байткод по одному потоку, остальные ждут — на 4 ядрах всё равно ~1 ядро работы плюс накладные на переключение. Ускорение дают процессы (multiprocessing) или C-расширения, отпускающие GIL (numpy).
это 9 из 67
Ещё 58 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.
Частые вопросы
Что спрашивают про GIL?
Что он даёт и что запрещает: одновременное выполнение байткода в нескольких потоках. Дальше обычно просят объяснить, почему многопоточность всё же полезна при работе с сетью и диском.
Когда выбирать процессы вместо asyncio?
Когда нагрузка вычислительная, а не ожидание ввода-вывода. Asyncio выигрывает на большом числе одновременных сетевых операций, процессы — на счёте.
Просят ли писать асинхронный код на собеседовании?
Иногда просят набросать код с gather и таймаутом или найти ошибку в готовом фрагменте: забытый await, блокирующий вызов, потерянное исключение.