GIL в Python изнутри: зачем он есть и что ломает его отключение
Global Interpreter Lock не даёт двум потокам CPython выполнять байткод одновременно. В 3.13 появился экспериментальный режим без GIL. Разбираем, почему блокировка вообще существует и что меняется, когда её убирают.

Возьмите CPU-bound задачу — например, перемножение больших матриц на чистом Python — и запустите её в четыре потока на восьмиядерной машине. Время выполнения не упадёт вчетверо. Оно останется примерно таким же, как в одном потоке, а иногда станет хуже из-за накладных расходов на переключение. Причина — Global Interpreter Lock (GIL): мьютекс, который позволяет выполнять байткод CPython только одному потоку в каждый момент времени. В Python 3.13 появился экспериментальный сборка без GIL (free-threaded build), и это первое за 30 лет изменение, которое реально трогает модель исполнения. Разбираем, что именно защищает GIL, почему его так долго не убирали и что ломается при его отключении.
Что такое GIL и что он на самом деле защищает
GIL — это не блокировка вашего кода. Это блокировка интерпретатора. Пока поток держит GIL, он выполняет байткод; чтобы другой поток начал выполняться, первый должен GIL отпустить. В результате многопоточная CPU-bound программа на CPython не масштабируется по ядрам, сколько бы потоков вы ни создали.
Защищает GIL в первую очередь подсчёт ссылок. Каждый объект в CPython хранит счётчик ob_refcnt. Когда на объект появляется новая ссылка, счётчик растёт; когда ссылка исчезает — уменьшается; при нуле объект освобождается. Операция refcnt += 1 не атомарна на уровне процессора: это чтение, инкремент и запись. Два потока без синхронизации могут прочитать одно и то же значение, оба прибавить единицу и записать результат, потерявший один инкремент. Итог — либо утечка памяти, либо преждевременное освобождение живого объекта и обращение к освобождённой памяти.
GIL решает это грубо, но дёшево: раз в каждый момент байткод исполняет один поток, гонок за ob_refcnt просто не возникает, и счётчик можно менять обычным инкрементом без атомарных операций.
GIL — не баг и не лень разработчиков. Это компромисс, который делает однопоточный Python быстрым, а C-расширения — простыми в написании. Цена компромисса — отсутствие параллелизма по ядрам для чистого Python-кода.
Когда GIL отпускается
Поток не держит GIL вечно. Он отпускает его в двух ситуациях. Первая — периодическое переключение: интерпретатор проверяет флаг переключения через заданный интервал (по умолчанию около 5 миллисекунд, значение задаётся через sys.setswitchinterval()). Вторая — блокирующие операции ввода-вывода: чтение файла, сетевой запрос, ожидание сокета. Перед такой операцией C-код отпускает GIL, а после — забирает обратно.
Именно поэтому I/O-bound программы на потоках работают хорошо даже с GIL: пока один поток ждёт ответа от сети, GIL свободен, и другой поток считает. Проблема только с CPU-bound нагрузкой, где потоки не отпускают GIL добровольно.
Почему GIL так долго нельзя было убрать
Убрать GIL пытались не раз. Самая известная ранняя попытка — патч Грега Стайна в конце 1990-х, где GIL заменили на множество мелких блокировок. Результат: однопоточный код замедлился примерно вдвое из-за постоянного захвата и освобождения этих замков. Для языка, где подавляющее большинство программ однопоточные, такой размен был неприемлем.
Корень проблемы — три вещи, завязанные на глобальную блокировку:
- Подсчёт ссылок. Без GIL каждый инкремент и декремент
ob_refcntдолжен стать атомарным или защищённым, а атомарные операции на разделяемой памяти дороже обычных и создают конкуренцию за кэш-линии между ядрами. - Внутренние структуры интерпретатора. Пулы объектов, кэши маленьких целых чисел и интернированных строк, аллокатор памяти — всё это разделяемое состояние, которое GIL неявно защищал.
- C-расширения. Тысячи библиотек (NumPy, драйверы БД, обёртки над C-кодом) написаны в предположении, что GIL их защищает. Убрать GIL — значит потенциально сломать этот код или заставить всех его переписать.
Именно баланс между этими тремя и держал GIL на месте десятилетиями: любое решение либо било по однопоточной скорости, либо ломало экосистему.
Что меняет free-threaded build в 3.13
Экспериментальный режим без GIL описан в PEP 703 и вошёл в CPython 3.13 как отдельная сборка. Это не поведение по умолчанию: нужна специально собранная версия интерпретатора (сборка с флагом --disable-gil), а сам режим на уровне рантайма помечен как экспериментальный. Проверить, отключён ли GIL, можно так:
# Python 3.13, только на free-threaded сборке
# На обычной сборке функции sys._is_gil_enabled может не быть
import sys
if hasattr(sys, "_is_gil_enabled"):
print("GIL включён:", sys._is_gil_enabled())
else:
print("Это обычная сборка с GIL")
Функция sys._is_gil_enabled() с ведущим подчёркиванием — приватная и завязана на конкретную сборку. Точный набор доступных API уточняйте в документации той версии CPython, которую вы собрали, а не считайте его стабильным.
Как убрали защиту подсчёта ссылок
Вместо одного GIL free-threaded сборка применяет несколько механизмов сразу:
- Biased reference counting. Счётчик разделён на быструю часть для потока-владельца объекта и медленную атомарную часть для остальных потоков. Владелец меняет счётчик дёшево, чужие потоки — через атомарные операции.
- Иммортализация. Долгоживущие объекты (
None,True,False, маленькие целые, интернированные строки) помечаются как бессмертные, и их счётчик вообще не трогают — гонки за него исключены. - Отложенный подсчёт ссылок для некоторых внутренних объектов, чтобы не платить за каждую операцию.
Внутренние структуры вроде аллокатора и разделяемых коллекций получили собственные мелкие блокировки взамен неявной защиты GIL.
Что это даёт и чем вы платите
| Аспект | Обычная сборка (с GIL) | Free-threaded (без GIL) |
|---|---|---|
| CPU-bound на потоках | Не масштабируется по ядрам | Масштабируется по ядрам |
| Однопоточная скорость | Базовая | Заметно ниже (накладные на подсчёт ссылок) |
| C-расширения | Работают без изменений | Нужна поддержка free-threaded, иначе GIL включается обратно |
| Гонки в чистом Python-коде | Скрыты атомарностью байткода | Проявляются, нужна явная синхронизация |
| Статус | Стабильный, по умолчанию | Экспериментальный |
Главная плата — однопоточная скорость. По оценкам авторов PEP 703, замедление отдельного потока измеряется процентами, но точная цифра зависит от нагрузки и версии, и её стоит мерить на своём коде, а не брать из статьи. Вторая плата — иллюзия безопасности исчезает. Раньше отдельные операции байткода были фактически атомарны за счёт GIL, и код вроде counter += 1 из нескольких потоков часто «случайно работал». Без GIL такие гонки становятся реальными, и синхронизация через threading.Lock перестаёт быть опциональной.
Где подход ломается
- C-расширения без пометки о поддержке. Если загруженный модуль не заявляет совместимость с free-threaded режимом, интерпретатор может автоматически включить GIL обратно на время работы — и всё масштабирование пропадёт. Это тихое поведение: программа работает, но параллелизма нет.
- Код, который полагался на атомарность GIL. Инкременты общих счётчиков, изменения общих словарей и списков без блокировок — под GIL это часто проходило, без GIL даёт повреждение данных.
- Конкуренция за кэш-линии. Даже при биасед-подсчёте частое изменение счётчиков объектов, разделяемых между ядрами, порождает false sharing и упирается в шину памяти. Идеального линейного ускорения по ядрам не будет.
- Экосистема пока не готова. Многие популярные библиотеки на момент выхода 3.13 ещё не собраны и не протестированы под free-threaded режим.
Что делать дальше
Если вы просто хотите ускорить CPU-bound задачу сегодня — free-threaded сборка ещё не тот инструмент, на который стоит опираться в проде. Проверенный путь остаётся прежним: multiprocessing или concurrent.futures.ProcessPoolExecutor, где параллелизм даётся отдельными процессами с собственными интерпретаторами. Для I/O-bound нагрузки потоки и asyncio работают и с GIL.
Если хотите пощупать режим без GIL — соберите или скачайте free-threaded сборку 3.13, прогоните на ней свой набор тестов и замерьте: ускорение на реальной CPU-нагрузке в потоках, замедление однопоточных участков, поведение при загрузке ваших C-зависимостей. Смотрите, не включается ли GIL обратно из-за несовместимого расширения. И держите в голове, что API вокруг режима ещё меняется от версии к версии — сверяйтесь с документацией конкретной сборки.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: PEP 703 – Making the Global Interpreter Lock Optional in CPython, Python 3.13 — What's New (free-threaded CPython)
Частые вопросы
GIL уже убрали в Python 3.13 по умолчанию?
А если у меня другая версия — 3.11 или 3.12?
Мой код на потоках работал годами. Он сломается без GIL?
Почему без GIL однопоточная программа становится медленнее?
Почему я включил free-threaded сборку, а ускорения по ядрам нет?
Стоит ли переходить на free-threaded сборку в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.