Почему IO-bound задачи не блокируют потоки и как работает GIL в Python
Что проверяют
Понимаете ли вы, что GIL защищает внутренности интерпретатора, а не ваши данные, и знаете ли, когда он отпускается. Следом обычно спрашивают, что выбрать под счётную нагрузку и что изменилось в свежих версиях Python.
Как отвечать
GIL это один мьютекс на интерпретатор в CPython: байт-код в каждый момент исполняет только один поток. Причина в подсчёте ссылок: он неатомарный, и без глобальной блокировки каждое изменение счётчика пришлось бы защищать отдельно.
Ключевая мысль: блокирующий системный вызов исполняет ядро, а не интерпретатор. Перед ним CPython отпускает GIL (в C-коде это макросы Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS) и забирает обратно после возврата. Поэтому пока поток ждёт ответа сокета, читает файл или спит в time.sleep, остальные потоки спокойно работают. Так же ведут себя C-расширения: NumPy отпускает GIL на тяжёлых операциях над массивами.
Для CPU-нагрузки картина обратная: потоки конкурируют за GIL, интерпретатор передаёт его каждые 5 мс (настраивается через sys.setswitchinterval), и на счётном коде два потока обычно медленнее одного. Отсюда стандартный выбор:
threading: ожидание внешних систем и блокирующие библиотеки без асинхронного аналога;asyncio: тысячи одновременных сетевых операций в одном потоке, дешевле по памяти;- процессы:
multiprocessingдля настоящей параллельности вычислений; - нативный код: NumPy, Polars или собственное расширение, отпускающее GIL.
Про версии: с Python 3.12 GIL стал отдельным для каждого субинтерпретатора, в 3.13 появилась сборка без GIL, а в 3.14 она стала официально поддерживаемой. Это отдельный билд, обычный CPython по-прежнему с GIL.
Где спотыкаются
Заявляют, что из-за GIL потоки в Python бесполезны. На нагрузке с ожиданием они работают, и вопрос как раз об этом.
Считают, что GIL делает код потокобезопасным. Он гарантирует целостность структур интерпретатора, но counter += 1 из двух потоков всё равно теряет обновления, нужен Lock или атомарная операция.
Уверены, что GIL уже убрали. Сборка без него ставится отдельно, часть C-расширений под неё не готова, а по умолчанию вы получаете обычный интерпретатор.