# Глава 29. GIL Кто-то называет GIL (global interpreter lock) главной архитектурной ошибкой питона. Кто-то — неизбежным компромиссом между скоростью выполнения однопоточных и многопоточных скриптов. А кто-то — вполне удачным решением, от которого бессмысленно отказываться. Разберём, что такое GIL, откуда у него растут ноги, какие на него планы у мейнтейнеров языка и что с этим делать простым разработчикам. А ещё поговорим о революционных изменениях, которые уже начались в Python 3.13 и продолжатся в Python 3.14: это опциональный GIL и субинтерпретаторы в стандартной библиотеке. ## Что такое GIL GIL, то есть глобальная блокировка интерпретатора, гарантирует, что в каждый момент времени байт-код скрипта исполняется только одним потоком ОС. Из-за GIL питон лишён истинной параллельности выполнения разных потоков. Даже на многоядерных CPU. При этом GIL не препятствует параллельности выполнения разных процессов: на каждый процесс скрипта запускается отдельный процесс интерпретатора со своим GIL. Для чего нужен GIL? При выполнении кода интерпретатор работает с потоко-небезопасными переменными. Чтобы гарантировать их сохранность, каждый поток интерпретатора должен захватывать и отпускать GIL. К таким переменным, например, относится счётчик ссылок. Счетчик ссылок в питоне — это основной механизм управления памятью, который отслеживает, сколько переменных указывают на конкретный объект в памяти. Его значение можно узнать для каждого объекта: ```python {.example_for_playground} from sys import getrefcount d = {} d2 = d print(getrefcount(d)) ``` ``` 3 ``` В примере у пустого словаря количество ссылок равно 3: на него ссылаются переменные `d` и `d2`, но откуда третья ссылка? Возвращаемое `getrefcount()` количество как правило на единицу больше, чем ожидается. Оно учитывает временную ссылку в качестве аргумента самой функции `getrefcount()`. Закономерно возникает вопрос: нельзя ли вместо блокирования потока целиком блокировать каждую потоко-небезопасную переменную отдельно? Синхронизация доступа к отдельным объектам вместо глобальной блокировки приводит к частым захватам/освобождениям этих самых блокировок. А это примерно [на 30%](https://docs.python.org/3/faq/library.html#can-t-we-get-rid-of-the-global-interpreter-lock) замедляет однопоточные скрипты. Поэтому разработчики языка сделали выбор в пользу глобальной блокировки. Пара важных фактов о GIL: - GIL не является частью языка. Это особенность реализации интерпретатора. Он есть в стандартном интерпретаторе _CPython_, написанном на C. Также он есть в [PyPy,](https://www.pypy.org/) написанном на языке _RPython_. Но GIL отсутствует в таких интерпретаторах как [_Jython_](https://www.jython.org/) (написан на Java) и [_IronPython_](https://ironpython.net/) (написан на C#). Там все проблемы синхронизации делегируются виртуальным машинам JVM и .NET/Mono. - GIL встречается в реализациях других интерпретируемых языков. Например, стандартная имплементация [Ruby](https://www.ruby-lang.org/en/) под названием Ruby MRI содержит GIL. Только называется он Global VM Lock. ## Следствия наличия GIL Как же правильно распараллеливать код, исполняющийся интерпретатором с GIL? Это зависит от специфики распараллеливаемых задач. {#block-cpu-bound} **CPU-bound** задачи — это вычисления, грузящие процессор: полнотекстовый поиск, обход графа, перемножение матриц и так далее. При использовании GIL-интерпретатора получить выигрыш в производительности за счёт потоков не получится. Оверхед на переключение контекста между потоками в связке с захватом и разблокировкой GIL могут сделать многопоточный код даже медленнее его однопоточной версии. Если требуется распараллелить CPU-bound работу, вместо потоков используйте процессы. **IO-bound** задачи, такие как обращение к внешнему API, работа с бд, файлами и консольным вводом-выводом, массу времени проводят в режиме ожидания данных. А блокирующее ожидание ввода-вывода заставляет поток отпустить GIL. Поэтому распараллеливание IO-bound на потоки всё же может принести выигрыш в скорости. Для достижения наилучших результатов количество потоков подбирается в зависимости от конфигурации целевой машины и специфики конкретной IO-bound задачи. Примерно так выглядит выполнение 2-х CPU-bound потоков на машине с единственным CPU. Иллюстрация взята из блога [Дэвида Бизли](https://dabeaz.blogspot.com/2010/01/python-gil-visualized.html) — разработчика, внёсшего большой вклад в развитие питона и сообщества вокруг него. ![2 CPU-bound потока на машине с 1 CPU.](https://raw.githubusercontent.com/senjun-team/senjun-courses/main/illustrations/python/2-threads-1-cpu.png) {.illustration} ![Легенда.](https://raw.githubusercontent.com/senjun-team/senjun-courses/main/illustrations/python/theads-cpu-legend.png) {.illustration} А теперь запустим 2 CPU-bound потока на машине с двумя CPU. ![2 CPU-bound потока на машине с 2 CPU.](https://raw.githubusercontent.com/senjun-team/senjun-courses/main/illustrations/python/2-threads-2-cpu.png) {.illustration} Иллюстрация демонстрирует, насколько неэффективно запускать несколько потоков питона в попытках распараллелить CPU-bound задачи. В интервалы времени, окрашенные красным, ОС передавала управление потоку, который пытался захватить GIL и ждал, пока его освободит другой поток. Подытожим: для распараллеливания CPU-bound задач подойдёт мультипроцессность, а для IO-bound задач — многопоточность. ## GIL и будущее питона: эра перемен На протяжении более 30 лет GIL был неотъемлемой частью _CPython_. Но ситуация начала меняться. На данный момент развиваются два глобальных направления разработки CPython и оба затрагивают GIL. ### Субинтерпретаторы (subinterpreters) О чём речь? Начиная с версии Python 1.5 [(то есть с 1997 года)](https://peps.python.org/pep-0554/#abstract) разработчики на питоне могли воспользоваться C-API для запуска нескольких экземпляров интерпретатора в рамках одного процесса. Эти интерпретаторы разделяли состояние: таблицу имён в глобальной области видимости, кэш импортированных модулей и т.д. Соответственно GIL тоже был общим. В версии языка Python 3.12 удалось добиться изоляции интерпретаторов внутри процесса: теперь у каждого из них своё состояние и свой собственный GIL. Но по-прежнему всё было доступно только через C-API. Python 3.14 меняет правила игры: принят [PEP 734](https://peps.python.org/pep-0734/), который добавляет модуль `concurrent.interpreters` в стандартную библиотеку. Теперь субинтерпретаторы доступны каждому питонисту без необходимости писать на C. Каждый субинтерпретатор имеет: - Собственное глобальное состояние (модули, `builtins` и т.д.) - Собственный GIL - Изоляцию памяти и пространства модулей от других интерпретаторов Это даёт настоящий параллелизм для CPU-bound задач без тяжёлых затрат на создание процессов и сериализацию данных. Взгляните на низкоуровневый API для запуска субинтерпретаторов: ```python {.example_for_playground} from concurrent import interpreters # Создание и запуск кода interp = interpreters.create() interp.exec("print('Hello from subinterpreter!')") # Обмен данными через кросс-интерпретаторную очередь q = interpreters.create_queue() interp.prepare_main(out=q) interp.exec(""" for i in range(5): out.put(i * i) """) # Получение результатов в основном интерпретаторе results = [] for _ in range(5): results.append(q.get()) print(results) # Обязательно закрываем интерпретатор interp.close() ``` ``` [0, 1, 4, 9, 16] ``` А это — высокоуровневый API для работы с субинтерпретаторами (аналог `concurrent.futures`): ```python {.example_for_playground} from concurrent.futures import InterpreterPoolExecutor def square(x): return x * x with InterpreterPoolExecutor(max_workers=4) as pool: results = list(pool.map(square, range(5))) print(results) ``` ``` [0, 1, 4, 9, 16] ``` Субинтерпретаторы занимают оптимальную позицию между потоками и процессами. Для распараллеливания кода по процессам используется модуль `multiprocessing`, который мы рассмотрим уже в следующей главе. А пока сравним его и субинтерпретаторы. Накладные расходы: - `multiprocessing`. Высокие (создание отдельных процессов ОС). - `subinterpreter`. Средние (легковесные интерпретаторы в одном процессе). Сериализация для обмена данными: - `multiprocessing`. Обязательная (модуль `pickle`), создаёт задержки. - `subinterpreter`. Отсутствует для поддерживаемых типов, обмен через очереди. Параллелизм: - `multiprocessing`. Истинный (разные процессы). - `subinterpreter`. Истинный (каждый субинтерпретатор имеет свой GIL). Изоляция: - `multiprocessing`. Полная (разные области памяти). - `subinterpreter`. Средняя (изолированные глобальные состояния, общий процесс). У субинтерпретаторов есть ограничения: - Передавать между интерпретаторами можно только [ограниченный набор типов](https://peps.python.org/pep-0554/#shareable-types): `None`, `bool`, `int`, `float`, `str`, `bytes`, `memoryview`. - C-расширения, рассчитанные на один интерпретатор, могут работать некорректно. - API всё ещё развивается, инструменты отладки и профилирования находятся в ранней стадии. ### Free-threaded Python Проект `nogil:` полный отказ от GIL. Исследователь-разработчик [Сэм Гросс](https://mail.python.org/archives/list/python-dev@python.org/thread/ABR2L6BENNA6UPSPKV474HCS4LWT26GY/) предложил реализацию CPython без GIL под названием [nogil.](https://github.com/colesbury/nogil) В ней удалось добиться удаления GIL таким образом, чтобы однопоточный код не потерял в производительности, а многопоточный хорошо масштабировался на ядра CPU. Уже принят [PEP 703](https://peps.python.org/pep-0703/), в рамках которого ведутся работы по внесению всех нужных изменений в CPython. Python 3.13 добавил экспериментальную сборку без GIL — так называемый free-threaded build. А Python 3.14 значительно улучшил эту поддержку. Если вы хотите поэкспериментировать с free-threaded Python, то выполните шаги: - Установите сборку через `pyenv` (версия с суффиксом `t`, например `3.13.5t` или `3.14t`) или официальный установщик Python.org (выбрать опцию free-threaded при кастомной установке). - Запустите код как обычно; истинная многопоточность активируется автоматически при использовании стандартных модулей (`threading`, `concurrent.futures.ThreadPoolExecutor`). Проверить, в каком режиме работает ваш интерпретатор, можно программно: ```python {.example_for_playground} import sys # Проверка, включён ли GIL (доступно в Python 3.13+) if hasattr(sys, '_is_gil_enabled'): print(f"GIL enabled: {sys._is_gil_enabled()}") else: print("Free-threaded build not detected") ``` ``` GIL enabled: True ``` В тестах на CPU-bound задачи (например, подсчёт простых чисел) 4 потока в free-threaded сборке Python 3.13 показали ускорение x3.4 относительно однопоточного базового уровня. Стандартная сборка с GIL в аналогичных условиях ускорения не даёт. Несмотря на впечатляющие результаты, free-threaded Python пока **не рекомендуется для критичных продакшен-систем**. Причины: - Многие популярные библиотеки (`NumPy`, `Pandas`, `SciPy`, `cryptography`, `lxml` и др.) требуют пересборки с флагом `Py_GIL_DISABLED`. Без адаптации возможны падения, гонки данных или снижение скорости. - Задачи, выполняющиеся в одном потоке, могут работать медленнее из-за атомарных операций и нового аллокатора. - Профилировщики и отладчики ещё не полностью адаптированы для free-threaded режима. Ситуация быстро улучшается: [`NumPy 2.3.0`](https://numpy.org/devdocs/release/2.3.0-notes.html) уже улучшил совместимость с free-threaded интерпретатором, а крупные компании вносят свой вклад в поддержку free-threading в популярные пакеты для научных вычислений (`numpy`, `scipy`, `scikit-learn`). ## Резюмируем - GIL (global interpreter lock) — блокировка интерпретатора, которая гарантирует, что в каждый момент времени интерпретатор исполняет только один поток скрипта на питоне. - GIL не позволяет эффективно использовать многоядерность путём распараллеливания на потоки. - Для ускорения выполнения IO-bound задач можно использовать пул потоков. Для ускорения CPU-bound лучше подойдёт пул процессов. - Python 3.13 добавил экспериментальный free-threaded build — сборку CPython без GIL, обеспечивающую истинную многопоточность. - Python 3.14 добавил модуль `concurrent.interpreters` — субинтерпретаторы в стандартной библиотеке с собственным GIL у каждого. - Оба направления развиваются параллельно: free-threaded режим даёт максимальную производительность для многопоточного кода, а субинтерпретаторы — стабильный API для CPU-bound параллелизма с изоляцией состояния. - Free-threaded Python пока экспериментальный: проверяйте совместимость библиотек и тщательно тестируйте перед использованием в продакшене.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!