Главная / Курсы / Python / Глава 29. GIL
# Глава 29. GIL Кто-то называет GIL (global interpreter lock) главной архитектурной ошибкой питона. Кто-то — неизбежным компромиссом между скоростью выполнения однопоточных и многопоточных скриптов. А кто-то — вполне удачным решением, от которого бессмысленно отказываться. В этой главе мы разберем, что такое GIL, откуда у него растут ноги, какие на него планы у мейнтейнеров языка и что с этим делать простым разработчикам. ## Что такое GIL GIL, то есть глобальная блокировка интерпретатора, гарантирует, что в каждый момент времени байт-код скрипта исполняется только одним потоком ОС. Из-за GIL питон лишен истинной параллельности выполнения разных потоков. Даже на многоядерных CPU. При этом GIL не препятствует параллельности выполнения разных процессов: на каждый процесс скрипта запускается отдельный процесс интерпретатора со своим GIL. Для чего нужен GIL? При выполнении кода интерпретатор работает с потоко-небезопасными переменными. Чтобы гарантировать их сохранность, каждый поток интерпретатора должен захватывать и отпускать GIL. К таким переменным, например, относится счетчик ссылок. Кстати, значение счетчика ссылок можно посмотреть для каждого объекта. ```python 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 и будущее питона На данный момент развиваются два глобальных направления разработки CPython. И оба затрагивают GIL. Полноценная поддержка **subinterpreters.** О чем речь? Начиная с версии 1.5 (то есть с 1997 года) разработчики на питоне могли воспользоваться C-API для запуска нескольких экземпляров интерпретатора в рамках одного процесса. Эти интерпретаторы разделяли состояние: таблицу имен в глобальной области видимости, кэш импортированных модулей и т.д. Соответственно GIL тоже был общим. В версии языка 3.12 удалось добиться изоляции интерпретаторов внутри процесса: теперь у каждого из них свое состояние и свой собственный GIL. Subinterpreters по-прежнему доступны только через C-API. А полноценная поддержка планируется в версии 3.13. Проект **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. ## Резюмируем - GIL (global interpreter lock) — блокировка интерпретатора, которая гарантирует, что в каждый момент времени интерпретатор исполняет только один поток скрипта на питоне. - GIL не позволяет эффективно использовать многоядерность путем распараллеливания на потоки. - Есть два направления развития языка, связанных с GIL: отказ от GIL (nogil) и изоляция GIL для каждого интерпретатора внутри потока (subinterpreters). - Для ускорения выполнения IO-bound задач можно использовать пул потоков. Для ускорения CPU-bound лучше подойдет пул процессов.
Отправка...

Если вам нравится проект, вы можете поддержать его!

Задонатить