Как GIL работает концептуально? Зачем вообще нужен GIL?
В CPython управление памятью основано на подсчёте ссылок: у каждого объекта есть счётчик ссылок, и когда он опускается до нуля, объект освобождается. Изменение этого счётчика из нескольких потоков без синхронизации приводит к гонкам, утечкам или крашам. Чтобы не ставить отдельные блокировки на каждый объект и не усложнять реализацию, CPython вводит одну глобальную блокировку интерпретатора — GIL.
Правило простое, но сильное: "Любой поток, который выполняет Python‑байткод или трогает Python‑объекты, должен сначала захватить GIL".
Это сильно упрощает потокобезопасность внутри интерпретатора и C‑расширений, но и задаёт ограничения на параллельность.
Что происходит при многопоточности?
Внутри одного процесса CPython потоки есть, ОС может планировать их на разные ядра, но:
В каждый момент времени только один поток владеет GIL и выполняет Python‑код.
GIL периодически переходит от одного потока к другому: интерпретатор через определённые интервалы времени даёт другим потокам шанс захватить GIL
Когда поток выполняет блокирующий I/O (например,
socket.recv(),file.read()), он отпускает GIL, позволяя другим потокам исполнять байт‑код, пока сам ждёт.
Поэтому важно разделять два типа нагрузок:
CPU‑bound — большинство времени тратится на чистые вычисления (парсинг, математика, обработка изображений и т.п.).
I/O‑bound — большинство времени тратится на ожидание внешних операций (сеть, диск, БД).
Когда GIL мешает, а когда — почти нет?
GIL — реальная проблема в CPU‑bound многопоточных сценариях. Ты запускаешь несколько потоков, каждый делает тяжёлые вычисления на чистом Python (например, поиск простых чисел, обработка больших массивов без NumPy). Логика в этих потоках почти не делает I/O и не отпускает GIL. В результате потоки вынуждены делить один GIL, в реальности на многоядерной машине ты почти не видишь ускорения, а иногда вообще получаешь замедление из‑за накладных расходов на переключения. Таким образом, в CPython многопоточность не даёт настоящего параллелизма для CPU‑bound задач — GIL не позволяет нескольким потокам одновременно крутить байт‑код на разных ядрах.
GIL почти не мешает в I/O‑bound многопоточных задачах. Поток инициирует I/O (HTTP‑запрос, чтение файла), интерпретатор отпускает GIL и блокируется на системном вызове. В это время другой поток может захватить GIL и крутить Python‑код. Поэтому многопоточность отлично подходит для параллельной обработки большого числа запросов к сети/диску, даже с GIL. GIL здесь почти не мешает, потому что время CPU‑вычислений невелико по сравнению с ожиданием I/O.
Поэтому в ответах на интервью часто говорят: многопоточность в Python хорошо подходит для I/O‑bound задач (много ожиданий сети/диска) и плохо — для CPU‑bound, где лучше использовать multiprocessing или выносить вычисления в C/NumPy.
Как обходят GIL в реальных проектах
На уровне middle важно уметь предложить варианты действий, если GIL мешает:
multiprocessing вместо threading - Каждый процесс имеет свой интерпретатор и свой GIL → процессы могут реально работать на разных ядрах. Минусы: межпроцессное взаимодействие тяжелее (IPC, сериализация), форк/спаун процессов дороже, чем создание потоков.
Использовать C‑расширения / NumPy / библиотеки, которые освобождают GIL - Многие библиотеки для работы с числами (NumPy, часть SciPy, некоторые крипто/ML‑библиотеки) реализуют тяжёлые вычисления в C/C++ и временно отпускают GIL, позволяя выполнять их параллельно.
Асинхронщина (asyncio) для большого числа I/O‑операций - Она не убирает GIL, но помогает структурировать конкурентное I/O‑bound выполнение в одном потоке.
Другие интерпретаторы - PyPy, Jython, IronPython и эксперименты с «no‑GIL» в CPython (пока не основная ветка) упоминают скорее как перспективу.
Как сформулировать ответ на уровне middle
В CPython GIL — это глобальный мьютекс интерпретатора, который гарантирует, что только один поток одновременно выполняет Python‑байткод. Это упростило управление памятью и сделало объекты потокобезопасными на уровне интерпретатора, но ограничило параллелизм: CPU‑bound код в нескольких потоках внутри одного процесса не может полноценно использовать несколько ядер. Для I/O‑bound задач GIL почти не мешает, так как при блокирующем I/O он отпускается и другие потоки могут выполняться. Если нужно распараллелить тяжёлые вычисления, обычно используют multiprocessing или выносят вычисления в C/NumPy, где GIL может быть отпущен.