В этой очередной статье по GIL разберемся, как работает Python, как был реализован GIL до версии языка 3.2, как глобальная блокировка работает сейчас, и что с ней делать.
Для чего нужен GIL?
Основная причина его существования — потокобезопасность. Например сборщик мусора работает на счетчике ссылок: каждая переменная или объект хранит число ссылок на него, и как только это число становится равным нулю, память освобождается. Если бы несколько потоков могли одновременно менять счётчик ссылок без синхронизации, это могло бы привести к повреждению данных или преждевременному удалению объектов.
Теоретически проблему можно решить, добавив отдельную блокировку ко всем структурам данных, которые могут использоваться разными потоками. Но это решение неэффективно, так как есть риск взаимоблокировок — ситуации, когда два или более потока ждут друг друга, захватив разные ресурсы, и ни один из них не может продолжить работу.
GIL устраняет эти сложности, действуя как одна глобальная блокировка на уровне интерпретатора, которая защищает весь процесс выполнения Python-кода: пока один поток исполняет байт-код, остальные ждут.
Для чего нужен GIL: проблема Race Condition
Проблема возникает с неатомарными операциями. Это операции, которые состоят из нескольких байт-кодов. Например, код x += 1 состоит из трёх операций:
Загрузка переменной и константы в стек.
Сложение и запись результата в стек.
Запись результата в переменную.
Если после первого шага поток отдаст выполнение кода другому потоку и не успеет записать результат, возникнет состояние гонки.
Простой пример для операции x += 1, которая выполняется в двух потоках:
Поток 1: загрузка переменной (0) и константы (1) в стек.
Поток 1: вычисление результата (0 + 1) и запись в стек (но не в переменную).
Передача управления Потоку 2.
Поток 2: загрузка переменной (0) и константы (1) в стек.
Поток 2: вычисление результата (0 + 1) и запись в стек.
Поток 2: запись результата (1) в переменную.
Передача управления Потоку 1.
Поток 1: запись результата (1) в переменную.
В итоге, в переменной х будет хранится единица, хотя ожидалась двойка, так как два потока должны увеличить значение на один, но в этом примере Поток 1 стирает результат работы Потока 2.
Конечно, это очень поверхностное описание гонки и атомарности операций. Например, здесь не сказано, как атомарные инструкции могут вызывать race condition. Однако такой простой код дает хорошее понимание, зачем нужен GIL.
Как работает Python
Понимание работы GIL невозможно без понимания работы самого Питона.
Байт код
Python-код «преобразуется» в байт-код. Это инструкции вроде BINARY_OP, STORE_NAME
и т.д. Эти инструкции выполняются виртуальной машиной. Посмотреть их можно в файлах с расширением *.pyc, либо воспользоваться модулем dis.
Например, строка a = 10 + 5
выполняется в несколько операций:
0 LOAD_CONST 0 (10) — загружаем константу 10 в стек;
2 LOAD_CONST 1 (5) — загружаем константу 5 в стек;
4 BINARY_OP 0 (+) — сложение;
6 STORE_NAME 0 (a) — сохранение результата в переменную а.
Виртуальная машина поочередно выполняет эти инструкции.
Вычислительный цикл
Когда запускается Python-программа, операционная система создает новый поток, который начинает выполнение функции main. Далее программа входит в вычислительный цикл. Это обычный бесконечный цикл for(;;), который обрабатывает и выполняет байт-код.
Внутри него находится большой оператор Switch, который выполняет код в зависимости от текущей инструкции байт-кода. Кроме того, в вычислительном цикле есть еще две важные операции:
Условие if — внутри if-а находится проверка флага
eval_breaker
. Если он установлен в true, поток прекращает выполнение байт-кода и освобождает GIL, об этом поговорим ниже.NEXTOPARG()
— получает следующую команду байт-кода.
В исходниках Python это выглядит примерно так:
for (;;)
if (eval_breaker)
//Если нужно, освобождаем GIL и прекращаем выполнение кода в текущем потоке.
//Получаем инструкцию байт-кода
opcode = NEXTOPARG();
//выполняем код в зависимости от полученной инструкции
switch (opcode)
case TARGET(NOP)
FAST_DISPATCH(); // следующая итерация
case TARGET(LOAD_FAST)
//код для загрузки локальной переменной
FAST_DISPATCH(); // следующая итерация
//еще 100+ case’ов
Создание нового потока
Чтобы создать новый поток в Python, используется метод start()
экземпляра класса threading.Thread
.
t = threading.Thread(target = some_func)
t.start()
Только что созданный поток начинает работу с Си-функции t_bootstrap()
, в качестве аргумента она принимает структуру boot
. Внутри структуры находится функция, которую мы передали в параметр target — some_func — а также её аргументы. Но самое важное, t_bootstrap()
захватывает GIL и начинает выполнение вычислительного цикла. Как происходит захват GIL, разберемся ниже.
Как работает GIL в Python
В общих чертах, GIL — это мьютекс, который установлен на уровне интерпретатора. В этой части разберемся, как это устроено в CPython.
GIL до Python 3.2
Хоть старые версии интерпретатора практически не используются, стоит разобраться, как работал GIL до релиза Питона 3.2.
Механизм блокировки был основан на тиках. Тик — это одна или несколько инструкций байт-кода. В каждом потоке есть счетчик тиков, когда их количество достигает 100, поток освобождает GIL и дает возможность поработать другому потоку. При этом, если поток начинает выполнять IO-Bound операцию, GIL освобождается автоматически.
Количество тиков, после которого освобождается GIL, можно изменить с помощью параметра sys.setcheckinterval().
Механизм, основанный на тиках, создает множество проблем. Как минимум, тик не зависит от времени, это лишь инструкция байт-кода, которая может выполняться довольно долго.
Вторая проблема. На многоядерных процессорах операционная система может выделить под каждый CPU-Bound поток отдельное ядро, чтобы операции выполнялись параллельно. Однако GIL создает ситуацию, когда один поток захватывает блокировку и начинает выполнение своего 100-тикового цикла. При этом потоки, для которых ОС выделила отдельные ядра, не могут выполнять свой код.
Довольно подробно старая версия GIL разобрана в этом видео: Inside the Python GIL.
GIL в Python 3.3
После версии Питона 3.2 GIL довольно сильно изменили и доработали. Сейчас переключение контекста происходит раз в 5 мс с помощью условной переменной. Это решает проблему независимости от времени, однако все еще не обеспечивает параллельное выполнение нескольких потоков.
Условная переменная — это примитив синхронизации, которые часто используют в многопоточном программировании в связке с мьютексами: первый поток захватывает мьютекс, а все остальные потоки вызывают cv_wait()
и ждут, пока не наступит определенное условие.
В Питоне это реализовано так:
Первый поток работает и ждет 5 мс. В это время второй поток ожидает в функции cv_wait(gil, timeout)
, где timeout — это 5 мс.

По истечении 5 мс устанавливается флаг gil_drop_request = 1
, который заставляет первый поток остановиться. Второй поток снова вызывает cv_wait()
.

Первый поток завершает выполнение последней инструкции, которую он обрабатывал и посылает первому потоку сигнал, например pthread_cond_signal
, который означает, что GIL освобожден.

Первый поток ожидает от второго потока повторного сигнала (ack), который означает, что GIL захвачен.

Когда первый поток получит этот сигнал, он заблокируется и будет ожидать в cv_wait()
. В этот момент второй поток выйдет из cv_wait
и начнет свою работу. При этом установиться gil_drop_request = 0
.

Довольно подробно GIL в 3.2 разобрано в этом видео: Understanding the Python GIL.
Как избавиться от GIL?
Возникает вопрос, если в Python глобальная блокировка мешает полноценно использовать многопоточность, то как тогда работать с ней? Основной способ — использовать C-расширения, но такой вариант подходит не всем, поэтому в небольших проектах часто прибегают к процессам и асинхронности.
Процессы
Легкий способ обойти ограничение GIL, если в вашей программе много CPU-bound операций. Модуль multiprocessing
позволяет запускать несколько независимых процессов, каждый из которых имеет собственный интерпретатор и память, что позволяет выполнять код параллельно.
from multiprocessing import Process
p1 = Process(target=some_function, args=("A",))
p2 = Process(target=some_function, args=("B",))
Однако у процессов есть свои недостатки. Порождение нового процесса значительно труднее, чем создание нового потока, а переключение контекста между процессами обходится системе дороже, чем между потоками. Кроме того, процессы не разделяют общую память: у каждого из них свой heap. Поэтому для синхронизации и обмена данными приходится использовать специальные механизмы — например, multiprocessing.Queue
или multiprocessing.SharedMemory
.
Асинхронное программирование
В Python основным инструментом для асинхронного программирования является модуль asyncio. Он построен на концепции корутин — специальных функций, которые можно приостанавливать и возобновлять. Корутина определяется с помощью ключевого слова async def, а управление ей передаётся через оператор await. Когда корутина встречает await, она «уступает» управление, позволяя выполняться другим задачам. Это делает возможным одновременное выполнение множества операций внутри одного потока без блокировки.
async def main():
await asyncio.gather(IO_task1(), IO_task2(), IO_task3())
asyncio.run(main())
За управление задачами отвечает цикл событий (event loop). Это своего рода диспетчер, который отслеживает, какие корутины готовы к выполнению, и по очереди передаёт им управление. Пока одна задача ждёт, например, ответа от сервера, цикл событий переключается на другие задачи. Таким образом достигается неплохая эффективность: один поток может обслуживать десятки соединений или операций ввода-вывода, не простаивая зря.
PEP 703
Также напоминаем, что в 2023 году появился PEP 703, который описывает инструменты для безопасной работы с многопоточностью без глобальной блокировки. Основные изменения уже разбирали много где, например, неплохо разобран PEP 703 здесь.
Комментарии (2)
KonstantinTokar
20.08.2025 10:53Так ли работает интерпретатор в питоне? А где создание новой версии "x"? Он же неизменяемый.
Например, код x += 1 состоит из трёх операций:
Загрузка переменной и константы в стек.
Сложение и запись результата в стек.
Запись результата в переменную.
zzzzzzerg
В мире принятого PEP734 (subinterpreters) не рассказать о них - преступление!
Все таки наверное 3.3 если вы упоминаете это видео в разделе про 3.3.
Это давно уже не так.
eval_breaker
содержит различные биты, которые управляют разными функциями.Так весь PEP703 (free-threading build) как раз про это.
Упоминания
gil_drop_request
остались только в комментариях.В целом не понятно о какой версии вы говорите.
За попытку плюс, за остальное - так себе.