GIL: не «Python не умеет в потоки», а «поток ждёт столько, сколько вы попросили»
Интервал переключения — настройка, которая работает: медиана ожидания следует за ней и на 5 и 50 мс отличается на единицы процентов. Пока не наступает случай, в котором она не работает вовсе — и он измеряется в сотнях миллисекунд.
Полное техническое изложение
TL;DR
- В Python есть правило: код исполняет только один поток за раз. Остальные ждут очереди.
- Ждут они не «сколько получится», а примерно столько, сколько записано в одной настройке. По умолчанию — пять миллисекунд.
- Но если тот, кто сейчас работает, начал одно длинное действие, прервать его некому. Тогда ждать приходится в десятки раз дольше — это измерено.
- Правило не действует, пока программа чего-то ждёт (сеть, диск) и пока считает библиотека вроде
zlib. В этих случаях потоки помогают, и помогают давно.
Один микрофон на всю переговорную
Представьте совещание. Людей восемь, микрофон один, правило одно: говорит тот, у кого микрофон в руках.
Никто не говорит медленно. Каждый произносит свою фразу с обычной скоростью. Но восемь фраз всё равно займут восемь очередей, а не одну — сколько бы человек в комнате ни сидело.
Это и есть GIL. Микрофон — право исполнять код Python. Люди — потоки.
import threading
def считать():
x = 0
for i in range(2_000_000):
x += i * i
# два потока считают половину работы каждый
t1 = threading.Thread(target=считать)
t2 = threading.Thread(target=считать)
t1.start(); t2.start()
t1.join(); t2.join()Такой код на двухъядерной машине не станет быстрее. Замер: один поток — 0,1516 с, два потока — 0,1559 с. Второй поток не помог, а немного помешал.
Регламент передачи
Микрофон передают по регламенту: примерно раз в пять миллисекунд. Это не выдумка автора, а настройка, которую видно:
import sys
sys.getswitchinterval() # 0.005И работает она предсказуемо. Если поставить в цикле замер «сколько я ждал своей очереди», получится вот что:
| Настройка | Сколько ждали на самом деле |
|---|---|
| 0,5 мс | 0,72 мс |
| 5 мс | 5,28 мс |
| 50 мс | 50,34 мс |
Что сюда поставили — примерно столько и ждём. На верхних двух строчках промах в единицы процентов. На нижней он больше — 0,72 вместо 0,5, — и причина понятная: чем чаще передают микрофон, тем заметнее время самой передачи.
Медиана 5.279 мс, p95 5.408 мс, максимум 9.020 мс по 283 замерам. Значение по умолчанию; сама настройка доступна с версии 3.2. Медиана ожидания — 5,279 мс при интервале 5 мс, то есть промах в 5,6 %. Настройка работает примерно так, как названа.
А теперь сюрприз
Регламент действует между действиями. Отобрать микрофон посреди одного действия некому.
Если работающий поток начал что-то длинное — например, отсортировал список из двух миллионов элементов, — все остальные ждут, пока он закончит. Настройка при этом остаётся прежней, пять миллисекунд, и не значит ничего.
| Чем занят работающий поток | Сколько ждут остальные |
|---|---|
| много коротких действий | 5,3 мс |
| одно умножение огромных чисел | 167,8 мс |
| одна сортировка большого списка | 185,6 мс (в худшем замере — 379,8) |
Разница — в десятки раз. И это не поломка: так и написано в исходниках CPython рядом с самой настройкой. Она «поощряет» переключение через заданное время, но не может его обеспечить, потому что одно действие может длиться сколько угодно.
Когда правило не действует
Самое главное, и это обычно теряется за фразой «потоки в Python бесполезны».
Пока программа ждёт, микрофон она отдаёт. Скачивание страницы, запрос к базе, чтение файла — всё это ожидание. Потоки здесь складываются почти идеально:
import threading, time
def подождать():
time.sleep(0.05)Восемь таких ожиданий подряд занимают 0,40 с. Восемь потоков — 0,05 с. Ускорение почти в восемь раз, и никаких ядер для этого не нужно: потоки ничего не считают, они ждут.
Некоторые библиотеки тоже отдают микрофон. Пока сжимает zlib, интерпретатор свободен. Замер: один поток сжимает буфер за 0,0564 с, два потока сжимают по такому же буферу каждый — и укладываются в 0,0553 с. Работы вдвое больше, времени столько же.
| Что делает программа | Сборка с GIL | Сборка без GIL |
|---|---|---|
Чистый Python: арифметика в цикле 4 000 000 итераций x += i * i, поделённые между потоками | 0.1516 с → 0.1559 с ×0.97 второй поток не дал ничего | 0.1754 с → 0.0998 с ×1.76 ускорение есть, но не двукратное |
Ожидание: time.sleep(0.05) восемь раз последовательно против восьми потоков | 0.4016 с → 0.0511 с ×7.86 потоки складываются почти идеально | 0.4014 с → 0.0519 с ×7.74 ровно то же самое |
C-расширение: zlib.compress буфер 5 999 872 байта; во второй колонке работы вдвое больше | 0.0564 с → 0.0553 с ×2.04 двойная работа за то же время | 0.0579 с → 0.0591 с ×1.96 то же самое, свободная сборка не нужна |
Байткод исполняется только с захваченным GIL. Два потока делят одно право исполнять — и делят время, а не складывают его.
tmp/gil-measure.py: пяти для строки с арифметикой, трёх для двух остальных. Нажатие на строку показывает, почему получилось именно так.Отсюда простое правило выбора:
- программа ждёт — берите потоки, они работают;
- программа считает библиотекой, которая отдаёт микрофон, — тоже потоки;
- программа считает на чистом Python — потоки не помогут, нужны процессы.
А GIL вообще убрали?
Частично и не по умолчанию.
С Python 3.13 существует отдельная сборка, собранная без GIL. В 3.14 она стала официально поддерживаемой — но не стала той, которую вы получаете, скачав Python с сайта. Обычная сборка по-прежнему с GIL, и решение о смене умолчания отложено до отдельного будущего документа, которого пока нет.
Проверить, какая сборка у вас:
import sys
sys._is_gil_enabled() # True в обычной сборке, False в свободнойВ свободной сборке тот же счётный код в два потока дал ускорение ×1,76 на двух ядрах. Не ×2 — за свободу платят: сама по себе такая сборка работает медленнее, и на плотных циклах разница доходила до 14–46 %.
И есть неприятная мелочь, которую стоит знать заранее: если два потока читают одни и те же объекты, в свободной сборке они начинают мешать друг другу. Замер по четырём прогонам: вдвоём выходит ×0,45–0,52, то есть медленнее, чем в одиночку, при вдвое большей работе. В обычной сборке те же прогоны дают ×0,94–1,00 — выигрыша нет, но и проигрыша тоже: потоки просто стоят в очереди.
Так что «уберём GIL и станет быстрее» — не работает как план. Работает другое: понять, чего ваша программа ждёт, а что считает.
TL;DR
- GIL не «замедляет потоки». Он заставляет их ждать очереди, и длительность ожидания задаётся
sys.setswitchinterval. Измерено: при интервале 5 мс медиана ожидания — 5,279 мс, при 50 мс — 50,335 мс, при 0,5 мс — 0,717 мс. На двух верхних значениях промах — единицы процентов, на нижнем уже 43 %: там начинает стоить само переключение. - Ровно до того момента, когда работающий поток входит в одну длинную операцию. Тогда интервал не значит ничего: то же ожидание становится 167,8 мс на умножении больших целых и 379,8 мс в худшем случае на
sorted(). Это не баг — так написано в комментарии рядом с самой константой. - «Потоки в Python бесполезны» — утверждение про чистый байткод и только про него. Измерено на тех же двух ядрах: чистый Python — ×0,97,
time.sleep— ×7,86,zlib.compress— ×2,04. - Свободные сборки — не гипотеза и не эксперимент: PEP 779 сделал их официально поддерживаемыми в 3.14. Но значением по умолчанию они не стали, и PEP про это говорит прямо — решение о фазе III «оставлено будущему PEP».
- Цена свободной сборки реальна и заметна на микротестах сильнее, чем на общем наборе бенчмарков. И у неё есть неочевидная часть: чтение объекта, созданного другим потоком, обходится дороже даже когда никакой конкуренции нет. Измерено: +15,4 % при полном отсутствии второго потока.
Зачем это знать?
Потому что про GIL спрашивают на собеседованиях, и правильный ответ там обычно неправильный.
«Глобальная блокировка интерпретатора не даёт потокам исполняться параллельно, поэтому для CPU-задач нужен multiprocessing» — это заученная формула, а не понимание. Она не объясняет, почему requests в двадцати потоках прекрасно работает, почему numpy считает на всех ядрах, и почему у вашего веб-сервера иногда появляются ответы в сотню-другую миллисекунд на пустом месте — при том что считать столько там нечего.
Ниже — четыре вещи, каждая из которых снята с работающего интерпретатора:
- на что влияет
sys.setswitchinterval(ответ прямее, чем кажется); - когда эта настройка не влияет ни на что;
- в каких случаях GIL вообще не участвует в происходящем;
- что именно изменилось со свободными сборками — и что не изменилось.
Все замеры скорости сделаны на CPython 3.13.7, в двух сборках одного и того же патча: обычной и собранной с --disable-gil (единственное исключение — таблица значений getswitchinterval() ниже, она снята с шести разных сборок). Машина: Linux x86_64, 2 ядра. Два ядра — важная деталь: все «ускорения» ниже упираются в потолок ×2, и я нигде не выдаю это за общий закон.
Модель в голове
Совещание в переговорной. Людей много, микрофон один, и правило простое: говорит тот, у кого микрофон.
Это не «люди медленно говорят». Каждый говорит с обычной скоростью. Но если восьмерым нужно сказать по фразе, восемь фраз займут восемь интервалов, а не один — сколько бы стульев в комнате ни стояло.
У правила есть три следствия, и все три проверяемы:
- Есть регламент. Микрофон передают примерно раз в пять миллисекунд. Именно «примерно»: договорённость есть, принуждения нет.
- Есть исключение. Пока человек не говорит, а слушает или ищет бумажку в портфеле, микрофон он отдаёт. Ожидание не занимает очередь.
- Есть дыра в регламенте. Если кто-то начал читать вслух длинный документ, отобрать микрофон посреди абзаца некому. Регламент говорит «пять миллисекунд», реальность — «пока не дочитает».
Дальше вся статья — это три этих следствия, переведённые в числа.
Что такое GIL буквально
Определение из глоссария Python описывает механизм, а не метафору: это способ, которым интерпретатор CPython обеспечивает, чтобы байткод в каждый момент исполнял только один поток. Там же сказано, зачем: это делает объектную модель, включая такие типы, как dict, неявно безопасной при одновременном доступе.
И в том же абзаце глоссария стоят две оговорки, мимо которых обычно проходят:
- некоторые модули расширений «спроектированы так, чтобы отпускать GIL при вычислительно тяжёлых задачах вроде сжатия или хеширования»;
- «GIL всегда отпускается при вводе-выводе».
То есть половина утверждения «Python не умеет в параллелизм» опровергнута в самом определении GIL. К этому вернёмся в третьем разделе.
Само переключение живёт в Python/ceval_gil.c. Там же — константа:
#define DEFAULT_INTERVAL 5000Пять тысяч микросекунд, то есть 5 мс. Это то самое значение, которое возвращает sys.getswitchinterval(), и оно одинаково во всех проверенных сборках:
| Сборка | sys.getswitchinterval() |
|---|---|
| 3.10.20 | 0.005 |
| 3.11.15 | 0.005 |
| 3.12.3 | 0.005 |
| 3.13.13 | 0.005 |
| 3.14.0rc2 | 0.005 |
| 3.13.7, сборка без GIL | 0.005 |
Последняя строка стоит отдельного взгляда: в сборке, где GIL выключен, настройка интервала переключения GIL по-прежнему существует и по-прежнему возвращает пять миллисекунд. Влияет ли она там на что-нибудь — вопрос, на который ниже есть измеренный ответ.
Первое: интервал работает буквально
Поставим опыт, который отвечает на вопрос «что вообще меняет setswitchinterval».
Два потока. Первый — «жадный»: считает в цикле и не отпускает интерпретатор добровольно. Второй — «быстрый»: в цикле делает time.sleep(0), то есть отдаёт управление и немедленно просит обратно, и записывает, сколько прошло. Записанное — это и есть время ожидания своей очереди.
def probe():
while not stop.is_set():
t0 = time.perf_counter()
time.sleep(0) # отдать GIL и попросить обратно
lags.append((time.perf_counter() - t0) * 1000)Медиана 5.279 мс, p95 5.408 мс, максимум 9.020 мс по 283 замерам. Значение по умолчанию; сама настройка доступна с версии 3.2. Медиана ожидания — 5,279 мс при интервале 5 мс, то есть промах в 5,6 %. Настройка работает примерно так, как названа.
Первые три столбца — зависимость, которую видно без всякой статистики:
| Интервал | Медиана ожидания | p95 | Замеров |
|---|---|---|---|
| 0,5 мс | 0,717 мс | 0,858 мс | 2026 |
| 5 мс (по умолчанию) | 5,279 мс | 5,408 мс | 283 |
| 50 мс | 50,335 мс | 50,636 мс | 30 |
На 5 и 50 мс медиана расходится с настройкой на единицы процентов: +5,6 % и +0,67 %. На 0,5 мс расхождение уже +43 % — и это тоже результат, а не погрешность: чем чаще передают управление, тем большую долю кванта занимает сама передача. Настройка задаёт нижнюю границу ожидания, а не точное значение.
Отсюда практический вывод, который редко формулируют явно: GIL не отнимает у вас процессорное время — он добавляет задержку. Если у вас веб-сервер на потоках и один эндпоинт считает что-то тяжёлое на чистом Python, все остальные запросы получают в свою латентность добавку порядка интервала переключения. Процессор при этом может быть свободен.
То же самое видно и с другой стороны — если смотреть не на ожидание, а на владение.
55 отрезков за 283 мс, медиана 5,127 мс. Значение по умолчанию. Медиана отрезка — 5,127 мс при интервале 5 мс, промах 2,5 %. Но один отрезок в этом окне длится 10,3 мс, вдвое дольше.
tmp/gil-timeline.py на CPython 3.13.7, Linux x86_64, 2 ядра. Владение восстановлено по отметкам времени, которые потоки ставят сами; шаг отметок — десятки микросекунд.Медиана отрезка владения при интервале по умолчанию — 5,127 мс. Совпадение с настройкой почти дословное.
Но в этом же окне есть отрезок длиной 10,3 мс — два кванта подряд. И это не погрешность измерения, а вход во вторую часть истории.
Второе: интервал ничего не гарантирует
Рядом с DEFAULT_INTERVAL в исходнике стоит оговорка, которую стоит прочитать целиком: механизм «encourages a defined switching period, but doesn't enforce it since opcodes can take an arbitrary time to execute».
Поощряет, но не обеспечивает — потому что одна операция может выполняться сколь угодно долго.
Документация sys.setswitchinterval говорит то же самое другими словами: значение задаёт идеальную длительность кванта, «actual value can be higher, especially if long-running internal functions or methods are used», и — отдельным предложением — «The interpreter doesn't have its own scheduler».
Проверим. Тот же опыт, тот же интервал 5 мс, но жадный поток теперь выполняет не миллион коротких операций, а одну длинную:
A = 7 ** 500_000 # 1 403 678 бит
B = 11 ** 500_000 # 1 729 716 бит
def one_big_mul():
A * B # одна операция, прервать её негде| Что делает жадный поток | Медиана ожидания | Максимум |
|---|---|---|
| много коротких операций | 5,293 мс | 6,598 мс |
| одно умножение больших целых | 167,756 мс | 217,305 мс |
один sorted() по 2 000 000 элементов | 185,619 мс | 379,826 мс |
Медиана третьей строки больше медианы первой в тридцать пять раз. Худший замер там же — 379,826 мс, то есть в семьдесят шесть раз больше настроенного интервала в 5 мс. Сам интервал при этом не менялся ни разу.
Механизм здесь простой и от этого неприятный. Запрос на передачу управления выставляется флагом, а проверяет этот флаг цикл интерпретатора — между операциями. Внутри одной операции проверять некому: sorted() уходит в C-код и возвращается оттуда, когда закончил. Никакой «вытесняющей многозадачности» на этом уровне нет, и sys.setswitchinterval её не добавляет.
Пять миллисекунд — это не гарантия задержки. Это гарантия того, что вас не заставят ждать дольше из-за одного лишь интервала. Всё остальное зависит от того, во что успел войти сосед.
Практический след этого наблюдения такой: если у вас в потоковом сервере где-то есть sorted() по большому списку, регулярное выражение по мегабайтному тексту или арифметика с большими целыми — уменьшение switchinterval не поможет. Помогает только вынести это из потока: в процесс, в очередь, в библиотеку, которая отпускает GIL.
Третье: когда GIL вообще ни при чём
Теперь к оговоркам из глоссария. Три вида работы, две сборки, одинаковый код.
| Что делает программа | Сборка с GIL | Сборка без GIL |
|---|---|---|
Чистый Python: арифметика в цикле 4 000 000 итераций x += i * i, поделённые между потоками | 0.1516 с → 0.1559 с ×0.97 второй поток не дал ничего | 0.1754 с → 0.0998 с ×1.76 ускорение есть, но не двукратное |
Ожидание: time.sleep(0.05) восемь раз последовательно против восьми потоков | 0.4016 с → 0.0511 с ×7.86 потоки складываются почти идеально | 0.4014 с → 0.0519 с ×7.74 ровно то же самое |
C-расширение: zlib.compress буфер 5 999 872 байта; во второй колонке работы вдвое больше | 0.0564 с → 0.0553 с ×2.04 двойная работа за то же время | 0.0579 с → 0.0591 с ×1.96 то же самое, свободная сборка не нужна |
Байткод исполняется только с захваченным GIL. Два потока делят одно право исполнять — и делят время, а не складывают его.
tmp/gil-measure.py: пяти для строки с арифметикой, трёх для двух остальных. Нажатие на строку показывает, почему получилось именно так.Читать эту таблицу стоит по строкам, а не по колонкам.
Чистый Python — ×0,97. Два потока сделали ту же работу за то же время — точнее, почти на три процента дольше: переключения не бесплатны. Это единственная строка, ради которой существует фраза «GIL мешает потокам».
Ожидание — ×7,86. Восемь time.sleep(0.05) последовательно занимают 0,4016 с, восемь потоков — 0,0511 с. Ядер для этого нужно ноль: потоки не считают, они ждут, а на время ожидания GIL отпущен. Именно поэтому «Python не умеет в потоки» никогда не мешало никому качать сто страниц одновременно.
zlib.compress — ×2,04. Здесь сравнение устроено иначе, и это важно: один поток сжимает буфер в 5 999 872 байта за 0,0564 с, два потока сжимают по такому же буферу каждый — и укладываются в 0,0553 с. Работы вдвое больше, времени столько же. Оба ядра заняты, GIL при этом не удерживается никем.
Почему так — написано в документации C API. Макрос Py_BEGIN_ALLOW_THREADS разворачивается в
{ PyThreadState *_save; _save = PyEval_SaveThread();а парный к нему Py_END_ALLOW_THREADS — в PyEval_RestoreThread(_save); }. Между ними C-код работает, не удерживая интерпретатор. Документация называет и конкретные модули: «the standard zlib and hashlib modules detach the thread state when compressing or hashing data».
Отсюда следует правило, которое стоит держать в голове при выборе библиотеки: вопрос не «на Python это или на C», а «отпускает ли эта функция GIL». Расширение на C, которое честно считает под удерживаемым GIL, для потоков ничем не лучше чистого Python — оно просто быстрее в один поток.
Свободные потоки: что уже правда
Здесь легко переусердствовать в обе стороны, поэтому — по датам и по документам.
PEP 703, Sam Gross, статус Final, решение 24 октября 2023, Python 3.13. Добавляет флаг сборки --disable-gil и прямо фиксирует границу: «The global interpreter lock will remain the default for CPython builds and python.org downloads». То есть уже в PEP, который «убирает GIL», сказано, что GIL остаётся значением по умолчанию.
PEP 779, Thomas Wouters, Matt Page и Sam Gross, статус Final, решение 16 июня 2025, Python 3.14. Это фаза II: свободная сборка официально поддержана, но по-прежнему опциональна. В разделах What's New для 3.14 это записано одной строкой: «PEP 779: Free-threaded Python is officially supported».
Фазы III — сделать свободную сборку значением по умолчанию — не произошло. PEP 779 говорит об этом сам: решение «very different, and we expect it will revolve around community support, willingness, and showing clear benefit. That's left for a future PEP». На момент написания такого PEP нет.
Что происходит прямо сейчас: 3.15 находится в стадии кандидата в релизы (rc1 вышел 4 августа 2026, финал назначен на 1 октября), и изменения там — про инструменты, а не про смену умолчания: PEP 803 добавляет стабильный ABI для свободных сборок (abi3t), у профилировщика появляется режим --mode gil, а семейство PyGILState_* объявлено мягко устаревшим — без планов удаления и с сохранением работоспособности существующего кода.
Проверить, где вы находитесь, можно из самого интерпретатора:
import sys, sysconfig
sys._is_gil_enabled() # False в свободной сборке
sysconfig.get_config_var("Py_GIL_DISABLED") # 1 в свободной сборке, 0 в обычной
sys.abiflags # "t" в свободной сборкеИ — обещанный ответ на вопрос из первого раздела: что делает sys.setswitchinterval в сборке, где GIL выключен. Ответ — ничего. Тот же замер ожидания, три разных интервала:
| Интервал | Медиана ожидания, сборка с GIL | Медиана ожидания, сборка без GIL |
|---|---|---|
| 0,5 мс | 0,717 мс | 0,067 мс |
| 5 мс | 5,279 мс | 0,067 мс |
| 50 мс | 50,335 мс | 0,067 мс |
Три одинаковых значения в правой колонке — это и есть содержательный результат. Настройка сохранилась, возвращает те же 0,005, и на поведение не влияет: очереди, длительностью которой она управляла, больше нет. Оставшиеся 0,067 мс — это уже стоимость самого time.sleep(0) и работы планировщика ОС, а не ожидание чего-либо в интерпретаторе.
И ещё одна деталь, о которой стоит знать заранее: свободная сборка умеет включать GIL обратно. Переменная PYTHON_GIL=1 или ключ -X gil=1 возвращают его. Проверено: PYTHON_GIL=1 python3.13t -c "import sys; print(sys._is_gil_enabled())" печатает True. Это не курьёз — так свободная сборка живёт с расширениями, которые к ней не готовы.
Чего свободная сборка стоит
Здесь начинается место, где легко соврать цифрой, поэтому сначала — источник, потом мои замеры, потом объяснение расхождения.
Официальный HOWTO называет цену прямо: «The free-threaded build has additional overhead when executing Python code compared to the default GIL-enabled build», и дальше — диапазон на наборе pyperformance: «from about 1% on macOS aarch64 to 8% on x86-64 Linux systems».
Мои замеры дали больше:
| Что мерялось | С GIL | Без GIL | Разница |
|---|---|---|---|
| арифметика в цикле, 6 000 000 операций | 0,2306 с | 0,2638 с | +14,4 % |
| чтение элементов списка в цикле в главном потоке, 3 000 000 раз | 0,1124 с | 0,1642 с | +46,1 % |
Это не опровержение официальной цифры, и подавать это как «на самом деле накладные расходы 46 %» было бы враньём. Разница объясняется тем, что мерялось: pyperformance — набор из разнородных программ, где интерпретатор занят не только исполнением байткода. Мои два теста — плотные циклы, состоящие ровно из тех операций, которые в свободной сборке подорожали. Это худший случай, а не средний.
Правильная формулировка звучит скучнее и полезнее: цена зависит от того, чем занята программа, и на плотном чистом байткоде она заметно выше средней по бенчмаркам.
Взамен свободная сборка даёт то, чего с GIL не бывает совсем: на тех же двух ядрах чистый Python в два потока дал ×1,76. Не ×2 — часть съедена той же самой ценой, что и в таблице выше, плюс машина двухъядерная и на ней есть кто-то ещё.
Неочевидная часть цены
А вот это в документации не написано, и на собеседованиях не спрашивают.
PEP 703 использует biased reference counting — подход, опирающийся на наблюдение, что «most objects are only accessed by a single thread, even in multi-threaded programs». У объекта есть поток-владелец, и операции со счётчиком ссылок из этого потока идут по быстрому пути, а из чужого — по общему.
Проверим, видно ли это снаружи. Опыт нарочно устроен так, чтобы конкуренции не было вообще: рабочий поток ровно один. Меняется только происхождение данных.
FOREIGN = make(0) # список создан в главном потоке
def worker_reads_foreign():
loop(FOREIGN, 3_000_000) # владелец объектов — другой поток
def worker_reads_own():
items = make(2) # список создан здесь же
loop(items, 3_000_000)| Сборка | Чужие объекты | Свои объекты | Отношение |
|---|---|---|---|
| с GIL | 0,1117 с | 0,1104 с | 1,012 |
| без GIL | 0,2057 с | 0,1783 с | 1,154 |
В сборке с GIL разницы нет — 1,2 % это шум. В свободной сборке чтение чужих объектов стоит на 15,4 % дороже, при том что второго потока не существует и бороться не с кем.
А если второй поток всё-таки появляется и оба читают одни и те же объекты, происходит вот что: два потока по 2 000 000 обращений каждый выполнили работу за 0,6108 с против 0,1376 с у одного потока. То есть вдвоём — медленнее, чем один, при вдвое большей работе итог ×0,45. По четырём прогонам это ×0,45–0,52.
Та же пара измерений в сборке с GIL даёт ×0,94–1,00 по тем же четырём прогонам. Разброс здесь больше самой величины, и это как раз и есть ответ: с GIL два потока на общих объектах не выигрывают ничего, но и не проигрывают — они просто стоят в очереди.
Практический вывод из этих трёх строк один, и он не про GIL: в свободной сборке важно, кто создал объект и кто его трогает. Разделяемое между потоками состояние из «неудобного» превращается в «дорогое», и «просто запустим в свободной сборке и получим ускорение» — не работает как стратегия.
Что с этим делать
Короткая карта решений, каждое привязано к измеренному выше.
Работа — ожидание (сеть, диск, база). Потоки уже работают, и уже давно. Измерено ×7,86 на восьми ожиданиях. GIL здесь не участвует. Свободная сборка ничего не добавит: ×7,74 — то же самое в пределах погрешности.
Работа — тяжёлые вычисления в библиотеке. Проверьте, отпускает ли она GIL. zlib и hashlib — да, это написано в документации; для остальных — смотрите на масштабирование, как в таблице выше. Если отпускает, потоки дадут ускорение по ядрам без всяких свободных сборок.
Работа — чистый Python, и её много. Процессы (multiprocessing, concurrent.futures.ProcessPoolExecutor) остаются рабочим ответом с 2008 года. Свободная сборка — второй ответ, и теперь официально поддержанный, но с оговорками из предыдущего раздела.
Проблема — не пропускная способность, а задержка. Вот здесь sys.setswitchinterval осмысленна, и таблица в начале статьи показывает, что именно вы покупаете и чем платите: уменьшение интервала в десять раз сократило медиану ожидания в 7,4 раза (5,279 → 0,717 мс) и во столько же увеличило число переключений (283 → 2026 замеров за то же время). Не в десять — часть выигрыша съедает сама передача управления. Но если задержку создаёт одна длинная операция, настройка бесполезна — это второй раздел.
И общее. Про GIL полезнее думать не как про «ограничение Python», а как про явную очередь, у которой есть настройка. Очередь видна, измеряется тремя строками кода и ведёт себя предсказуемо ровно до границы, которая тоже описана — в комментарии рядом с константой, задающей интервал.
Частые заблуждения
«GIL делает потоки в Python бесполезными».
Бесполезными — только для чистого байткода. Измерено на одной и той же машине в два ядра: арифметика в цикле даёт ×0,97, восемь time.sleep(0.05) — ×7,86, zlib.compress — ×2,04 (два потока сжимают по полному буферу каждый и укладываются в время одного). Две из трёх строк — про то, что GIL в происходящем не участвует: на вводе-выводе он отпускается всегда, а zlib и hashlib отсоединяют состояние потока на время работы, и это записано в документации C API.
«sys.setswitchinterval — тонкая настройка, эффект от неё непредсказуем».
Эффект прямой и воспроизводимый. Поток, который просит управление немедленно, ждёт примерно столько, сколько указано: медиана 0,717 мс при интервале 0,5 мс, 5,279 мс при 5 мс, 50,335 мс при 50 мс. На двух верхних значениях промах — единицы процентов; на 0,5 мс уже 43 %, потому что там ощутима стоимость самой передачи управления. Непредсказуемым при этом становится не эффект настройки, а поведение соседа: если он вошёл в одну длинную операцию, настройка не действует вовсе.
«Интерпретатор переключает потоки каждые 5 мс».
Он выставляет запрос на переключение через 5 мс, а проверяется этот запрос между операциями. Внутри одной операции проверять некому. Измерено при неизменном интервале в 5 мс: пока жадный поток выполняет много коротких операций, ожидание — 5,293 мс; на одном умножении целых чисел в 1,4 и 1,7 миллиона бит — 167,756 мс; на одном sorted() по двум миллионам элементов — 185,619 мс, в худшем замере 379,826 мс. В Python/ceval_gil.c рядом с DEFAULT_INTERVAL это сказано прямым текстом: механизм «encourages a defined switching period, but doesn't enforce it».
«В Python 3.13 GIL убрали».
В 3.13 появилась отдельная сборка с флагом --disable-gil, и сам PEP 703 фиксирует границу: GIL остаётся значением по умолчанию для сборок CPython и загрузок с python.org. Официально поддержанной свободная сборка стала в 3.14 (PEP 779) — это фаза II, «supported but still optional». Фаза III, то есть смена умолчания, в PEP 779 прямо оставлена будущему документу: «That's left for a future PEP». Такого PEP на момент написания нет.
«Свободная сборка — это просто более быстрый Python».
Она медленнее в один поток и не всегда быстрее в два. Официальный HOWTO называет диапазон 1–8 % на наборе pyperformance; на плотных циклах измерено больше — +14,4 % на арифметике и +46,1 % на чтении элементов списка, и это худший случай, а не средний. Ускорение при этом реальное: те же вычисления в два потока на двух ядрах дали ×1,76. Но два потока, читающие одни и те же объекты, дали ×0,45–0,52 по четырём прогонам — вдвоём медленнее, чем один; в сборке с GIL те же прогоны дают ×0,94–1,00, то есть ни выигрыша, ни проигрыша.
«Без GIL все объекты становятся равноправными».
Нет: у объекта появляется поток-владелец. PEP 703 использует biased reference counting, опираясь на то, что «most objects are only accessed by a single thread». Разницу видно даже без конкуренции. Опыт с ровно одним рабочим потоком: чтение объектов, созданных в другом потоке, заняло 0,2057 с против 0,1783 с для собственных — на 15,4 % дороже. В сборке с GIL те же два случая различаются на 1,2 %, то есть не различаются.
«Свободная сборка не умеет работать со старыми расширениями, поэтому её нельзя внедрять постепенно».
Умеет: GIL в ней включается обратно. Переменная PYTHON_GIL=1 или ключ -X gil=1 возвращают его, и sys._is_gil_enabled() начинает печатать True — проверено на 3.13.7t. В 3.15 к этому добавляется PEP 803: стабильный ABI для свободных сборок (abi3t), из-за которого расширению больше не нужно собираться отдельно под каждую версию.
Проверка знаний
Веб-сервер на потоках. Один эндпоинт считает что-то тяжёлое на чистом Python. Что произойдёт с остальными запросами при настройках по умолчанию?
Источники и что читать дальше
10 ИСТОЧНИКОВ
- Глоссарий Python — global interpreter lock, free threading, free-threaded buildОфициальная документация. Определение GIL слово в слово, включая две оговорки, вокруг которых строится половина статьи: расширения могут отпускать GIL на тяжёлых вычислениях, а на вводе-выводе он отпускается всегда.https://docs.python.org/3/glossary.html#term-global-interpreter-lock
- Python/ceval_gil.c — DEFAULT_INTERVAL и gil_drop_requestИсходный код CPython. #define DEFAULT_INTERVAL 5000 (микросекунды) и оговорка рядом: интервал «encourages a defined switching period, but doesn't enforce it since opcodes can take an arbitrary time to execute». Тег CPython 3.14.0.https://github.com/python/cpython/blob/v3.14.0/Python/ceval_gil.c
- sys.setswitchinterval и sys.getswitchintervalОфициальная документация. «This floating-point value determines the ideal duration of the timeslices» и прямое предупреждение: «The interpreter doesn't have its own scheduler». Дефолтное значение в документации не названо.https://docs.python.org/3/library/sys.html#sys.setswitchinterval
- C API — Thread State and the Global Interpreter LockОфициальная документация. Во что именно разворачивается Py_BEGIN_ALLOW_THREADS, и прямое указание, что zlib и hashlib отсоединяют состояние потока на время сжатия и хеширования.https://docs.python.org/3/c-api/threads.html
- Python support for free threading (HOWTO)Официальная документация. sys._is_gil_enabled(), переменная PYTHON_GIL и -X gil, и названная цена свободной сборки: «from about 1% on macOS aarch64 to 8% on x86-64 Linux systems» на наборе pyperformance.https://docs.python.org/3/howto/free-threading-python.html
- PEP 703 — Making the Global Interpreter Lock Optional in CPythonPEP. Sam Gross, Python 3.13, статус Final, решение 24 октября 2023. Отсюда biased reference counting и прямая фраза, что GIL остаётся значением по умолчанию для сборок CPython и загрузок с python.org.https://peps.python.org/pep-0703/
- PEP 779 — Criteria for supported status for free-threaded PythonPEP. Thomas Wouters, Matt Page, Sam Gross; Python 3.14, статус Final, решение 16 июня 2025. Фаза II: официально поддержано, но по-прежнему опционально. Про фазу III сказано прямо: «That's left for a future PEP».https://peps.python.org/pep-0779/
- What's New In Python 3.14Официальная документация. Формулировка «PEP 779: Free-threaded Python is officially supported» — момент, когда сборка перестала считаться экспериментальной.https://docs.python.org/3/whatsnew/3.14.html
- What's New In Python 3.15Официальная документация. PEP 803 («abi3t» — стабильный ABI для свободных сборок), режим --mode gil у профилировщика Tachyon и мягкое устаревание семейства PyGILState. Проверено на предварительной документации 3.15.https://docs.python.org/3.15/whatsnew/3.15.html
- PEP 790 — Python 3.15 Release SchedulePEP. Нужен, чтобы честно назвать статус 3.15 на момент написания: rc1 вышел 4 августа 2026, финальный релиз назначен на 1 октября 2026.https://peps.python.org/pep-0790/