Deep Engineering
Поиск
Экспертный·Опубликовано·3.13 · 3.14 · 3.15·30 МИН

GIL: не «Python не умеет в потоки», а «поток ждёт столько, сколько вы попросили»

Интервал переключения — настройка, которая работает: медиана ожидания следует за ней и на 5 и 50 мс отличается на единицы процентов. Пока не наступает случай, в котором она не работает вовсе — и он измеряется в сотнях миллисекунд.

Полное техническое изложение

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 считает на всех ядрах, и почему у вашего веб-сервера иногда появляются ответы в сотню-другую миллисекунд на пустом месте — при том что считать столько там нечего.

Ниже — четыре вещи, каждая из которых снята с работающего интерпретатора:

  1. на что влияет sys.setswitchinterval (ответ прямее, чем кажется);
  2. когда эта настройка не влияет ни на что;
  3. в каких случаях GIL вообще не участвует в происходящем;
  4. что именно изменилось со свободными сборками — и что не изменилось.

Все замеры скорости сделаны на CPython 3.13.7, в двух сборках одного и того же патча: обычной и собранной с --disable-gil (единственное исключение — таблица значений getswitchinterval() ниже, она снята с шести разных сборок). Машина: Linux x86_64, 2 ядра. Два ядра — важная деталь: все «ускорения» ниже упираются в потолок ×2, и я нигде не выдаю это за общий закон.

Модель в голове

Совещание в переговорной. Людей много, микрофон один, и правило простое: говорит тот, у кого микрофон.

Это не «люди медленно говорят». Каждый говорит с обычной скоростью. Но если восьмерым нужно сказать по фразе, восемь фраз займут восемь интервалов, а не один — сколько бы стульев в комнате ни стояло.

У правила есть три следствия, и все три проверяемы:

  • Есть регламент. Микрофон передают примерно раз в пять миллисекунд. Именно «примерно»: договорённость есть, принуждения нет.
  • Есть исключение. Пока человек не говорит, а слушает или ищет бумажку в портфеле, микрофон он отдаёт. Ожидание не занимает очередь.
  • Есть дыра в регламенте. Если кто-то начал читать вслух длинный документ, отобрать микрофон посреди абзаца некому. Регламент говорит «пять миллисекунд», реальность — «пока не дочитает».

Дальше вся статья — это три этих следствия, переведённые в числа.

Что такое GIL буквально

Определение из глоссария Python описывает механизм, а не метафору: это способ, которым интерпретатор CPython обеспечивает, чтобы байткод в каждый момент исполнял только один поток. Там же сказано, зачем: это делает объектную модель, включая такие типы, как dict, неявно безопасной при одновременном доступе.

И в том же абзаце глоссария стоят две оговорки, мимо которых обычно проходят:

  • некоторые модули расширений «спроектированы так, чтобы отпускать GIL при вычислительно тяжёлых задачах вроде сжатия или хеширования»;
  • «GIL всегда отпускается при вводе-выводе».

То есть половина утверждения «Python не умеет в параллелизм» опровергнута в самом определении GIL. К этому вернёмся в третьем разделе.

Само переключение живёт в Python/ceval_gil.c. Там же — константа:

C
#define DEFAULT_INTERVAL 5000

Пять тысяч микросекунд, то есть 5 мс. Это то самое значение, которое возвращает sys.getswitchinterval(), и оно одинаково во всех проверенных сборках:

Сборкаsys.getswitchinterval()
3.10.200.005
3.11.150.005
3.12.30.005
3.13.130.005
3.14.0rc20.005
3.13.7, сборка без GIL0.005

Последняя строка стоит отдельного взгляда: в сборке, где GIL выключен, настройка интервала переключения GIL по-прежнему существует и по-прежнему возвращает пять миллисекунд. Влияет ли она там на что-нибудь — вопрос, на который ниже есть измеренный ответ.

Первое: интервал работает буквально

Поставим опыт, который отвечает на вопрос «что вообще меняет setswitchinterval».

Два потока. Первый — «жадный»: считает в цикле и не отпускает интерпретатор добровольно. Второй — «быстрый»: в цикле делает time.sleep(0), то есть отдаёт управление и немедленно просит обратно, и записывает, сколько прошло. Записанное — это и есть время ожидания своей очереди.

PYTHON
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 %. Настройка работает примерно так, как названа.

CPython 3.13.7, Linux x86_64, 2 ядра. Синим — прогоны, где жадный поток выполняет много коротких операций; красным — где он выполняет одну длинную. Шкала общая, поэтому первые три столбца рядом с последними почти не видны: это и есть разница между «настройка работает» и «настройка ни при чём».

Первые три столбца — зависимость, которую видно без всякой статистики:

ИнтервалМедиана ожидания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, все остальные запросы получают в свою латентность добавку порядка интервала переключения. Процессор при этом может быть свободен.

То же самое видно и с другой стороны — если смотреть не на ожидание, а на владение.

поток 0
поток 1
0 мс15 мс30 мс45 мс60 мс

55 отрезков за 283 мс, медиана 5,127 мс. Значение по умолчанию. Медиана отрезка — 5,127 мс при интервале 5 мс, промах 2,5 %. Но один отрезок в этом окне длится 10,3 мс, вдвое дольше.

Окно 60 мс из прогона 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 мс, но жадный поток теперь выполняет не миллион коротких операций, а одну длинную:

PYTHON
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. Два потока делят одно право исполнять — и делят время, а не складывают его.

CPython 3.13.7, обе сборки одного патча, Linux x86_64, 2 ядра. Каждое значение — минимум из нескольких прогонов 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 разворачивается в

C
{ 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_* объявлено мягко устаревшим — без планов удаления и с сохранением работоспособности существующего кода.

Проверить, где вы находитесь, можно из самого интерпретатора:

PYTHON
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». У объекта есть поток-владелец, и операции со счётчиком ссылок из этого потока идут по быстрому пути, а из чужого — по общему.

Проверим, видно ли это снаружи. Опыт нарочно устроен так, чтобы конкуренции не было вообще: рабочий поток ровно один. Меняется только происхождение данных.

PYTHON
FOREIGN = make(0)                 # список создан в главном потоке
 
def worker_reads_foreign():
    loop(FOREIGN, 3_000_000)      # владелец объектов — другой поток
 
def worker_reads_own():
    items = make(2)               # список создан здесь же
    loop(items, 3_000_000)
СборкаЧужие объектыСвои объектыОтношение
с GIL0,1117 с0,1104 с1,012
без GIL0,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), из-за которого расширению больше не нужно собираться отдельно под каждую версию.

Проверка знаний

Вопрос 1 из 5

Веб-сервер на потоках. Один эндпоинт считает что-то тяжёлое на чистом Python. Что произойдёт с остальными запросами при настройках по умолчанию?

Источники и что читать дальше

10 ИСТОЧНИКОВ

  1. Глоссарий Python — global interpreter lock, free threading, free-threaded buildОфициальная документация. Определение GIL слово в слово, включая две оговорки, вокруг которых строится половина статьи: расширения могут отпускать GIL на тяжёлых вычислениях, а на вводе-выводе он отпускается всегда.https://docs.python.org/3/glossary.html#term-global-interpreter-lock
  2. 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
  3. 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
  4. C API — Thread State and the Global Interpreter LockОфициальная документация. Во что именно разворачивается Py_BEGIN_ALLOW_THREADS, и прямое указание, что zlib и hashlib отсоединяют состояние потока на время сжатия и хеширования.https://docs.python.org/3/c-api/threads.html
  5. 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
  6. 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/
  7. 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/
  8. What's New In Python 3.14Официальная документация. Формулировка «PEP 779: Free-threaded Python is officially supported» — момент, когда сборка перестала считаться экспериментальной.https://docs.python.org/3/whatsnew/3.14.html
  9. 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
  10. PEP 790 — Python 3.15 Release SchedulePEP. Нужен, чтобы честно назвать статус 3.15 на момент написания: rc1 вышел 4 августа 2026, финальный релиз назначен на 1 октября 2026.https://peps.python.org/pep-0790/