В чем фундаментальная разница между asyncio и threading и почему выбирать одно вместо другого?
Что проверяют
Понимаете ли вы модель выполнения каждого подхода и роль GIL, а не только лозунг «asyncio для I/O». Типичное продолжение: что будет, если внутри корутины вызвать requests.get или time.sleep, и что брать для тяжёлых вычислений.
Как отвечать
Фундаментальная разница в том, кто решает, когда переключаться:
threading: потоки ОС с вытесняющей многозадачностью. В CPython поток отдаёт GIL по таймеру (sys.getswitchinterval(), по умолчанию 5 мс) и на блокирующем вводе-выводе. Подходит для обычных синхронных библиотек:requests,psycopg2;asyncio: один поток и цикл событий, кооперативная многозадачность. Корутина отдаёт управление только в точкеawait. Тысячи соединений обходятся дешевле тысяч потоков, но нужны асинхронные библиотеки:httpx,aiohttp,asyncpg.
Как выбирать:
- много сетевого ожидания и есть async-стек:
asyncio, например FastAPI-сервис или сборщик, который ходит в сотни API; - ввод-вывод на синхронных библиотеках:
ThreadPoolExecutor, переписывать всё на async ради этого не нужно; - вычисления: ни то ни другое. В обычной сборке CPython GIL не даёт потокам считать параллельно, поэтому берут
ProcessPoolExecutorили NumPy и C-расширения, отпускающие GIL. Сборка без GIL (free-threaded) в 3.14 официально поддерживается, но остаётся опциональной, и не все расширения с ней совместимы.
Мост между мирами: asyncio.to_thread() запускает блокирующую функцию в пуле потоков, не останавливая цикл событий.
Где спотыкаются
Советуют threading для CPU-bound задач. В стандартном CPython расчёт это не ускорит, а иногда и замедлит из-за борьбы за GIL.
Вызывают блокирующий код внутри async def. Один time.sleep(1) или синхронный запрос к базе замораживает весь цикл событий, и сервис перестаёт отвечать всем клиентам сразу.
Считают, что в asyncio не бывает гонок. Между двумя await другая корутина может изменить общее состояние, поэтому asyncio.Lock существует не просто так.