Как сделать асинхронную программу более многопоточной?
Что проверяют
Понимаете ли вы, что asyncio и потоки решают разные задачи, и умеете ли сначала найти узкое место. Следом обычно спрашивают про GIL: помогут ли потоки, если нагрузка вычислительная.
Как отвечать
Начните с уточнения: event loop в asyncio работает в одном потоке, и «добавить потоков» имеет смысл только под конкретную проблему. Их три:
- блокирующий вызов внутри корутины:
requests,time.sleep, синхронный драйвер базы. Он останавливает все задачи сразу. Лучшее решение: заменить на асинхронную библиотеку (httpx,aiohttp,asyncpg). Если замены нет, вынести вызов в поток черезasyncio.to_thread(с Python 3.9) илиloop.run_in_executor. Пул по умолчанию ограниченmin(32, os.cpu_count() + 4)потоками; - CPU-bound работа: парсинг, сжатие, вычисления. Потоки в обычной сборке CPython её не ускорят из-за GIL, нужен
ProcessPoolExecutorчерез тот жеrun_in_executor. Аргументы и результат при этом сериализуются через pickle, это накладной расход; - один процесс упёрся в ядро: запускают несколько процессов с отдельными event loop, например несколько воркеров uvicorn или gunicorn.
Упомяните free-threaded сборку CPython без GIL: экспериментальной она появилась в 3.13, в 3.14 стала официально поддерживаемой, но по умолчанию не ставится, и не все C-расширения с ней совместимы.
Найти блокировки помогает debug-режим: asyncio.run(main(), debug=True) пишет в лог колбэки дольше 100 мс. Одновременность ограничивают asyncio.Semaphore, группы задач запускают через asyncio.TaskGroup (3.11+).
Где спотыкаются
Считают, что потоки ускорят числодробилку. В обычном CPython она упрётся в GIL и быстрее не станет, а из-за переключений может и замедлиться.
Вызывают синхронную функцию прямо в корутине. Код формально асинхронный, а работает последовательно.
Трогают объекты asyncio из чужого потока. Они не потокобезопасны, для этого есть asyncio.run_coroutine_threadsafe и loop.call_soon_threadsafe. И забывают, что отмена задачи не останавливает уже запущенную в пуле функцию.