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

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.70.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)

На диаграмме выше три синих столбика — та же зависимость, видимая без всякой статистики. Вот те же числа таблицей:

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

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

Медиана отрезка владения при интервале по умолчанию — 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 мс

Первая строка — отдельный прогон того же опыта, поэтому она не совпадает дословно с 5,279 мс из таблицы выше.

Медиана третьей строки больше медианы первой в тридцать пять раз. Худший замер там же — 379,826 мс, то есть в семьдесят шесть раз больше настроенного интервала в 5 мс. Сам интервал при этом не менялся ни разу.

Механизм здесь простой и от этого неприятный. Запрос на передачу управления выставляется флагом, а проверяет этот флаг цикл интерпретатора — между операциями. Внутри одной операции проверять некому: sorted() уходит в C-код и возвращается оттуда, когда закончил. Никакой «вытесняющей многозадачности» на этом уровне нет, и sys.setswitchinterval её не добавляет.

Пять миллисекунд — это не гарантия задержки. Это гарантия того, что вас не заставят ждать дольше из-за одного лишь интервала. Всё остальное зависит от того, во что успел войти сосед.

—

Практический след этого наблюдения такой: если у вас в потоковом сервере где-то есть sorted() по большому списку, регулярное выражение по мегабайтному тексту или арифметика с большими целыми — уменьшение switchinterval не поможет. Помогает только вынести это из потока: в процесс, в очередь, в библиотеку, которая отпускает GIL.

Третье: когда GIL вообще ни при чём

Теперь к оговоркам из глоссария. Три вида работы, две сборки, одинаковый код.

Читать эту таблицу стоит по строкам, а не по колонкам.

Чистый 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 (стандартные модули zlib и hashlib отсоединяют состояние потока на время сжатия или хеширования данных).

Отсюда следует правило, которое стоит держать в голове при выборе библиотеки: вопрос не «на Python это или на C», а «отпускает ли эта функция GIL». Расширение на C, которое честно считает под удерживаемым GIL, для потоков ничем не лучше чистого Python — оно просто быстрее в один поток.

Почему «отпускает GIL» не значит «масштабируется»

В таблице выше zlib.compress дал ×2,04 на двух ядрах — то есть ровно то, чего и ждут. Но такой ответ получается не всегда, и совет «смотрите на масштабирование» полезнее превратить в список причин, по которым его не будет. Три причины документированы прямо; четвёртая, про которую чаще всего говорят, в первоисточниках не подтверждается — и об этом честно ниже.

Первая: порог, ниже которого GIL не отпускается вообще. Формулировка «модуль отпускает GIL» бывает верна и при этом бесполезна:

To allow multithreading, the Python GIL is released while computing a hash supplied more than 2047 bytes of data at once in its constructor or update method.

перевод

Чтобы допустить многопоточность, GIL Python отпускается при вычислении хеша, если за один раз в конструктор или в метод update передано более 2047 байт данных.

— hashlib — документация

Две тысячи байт — это порог на один вызов, а не на объём работы. Хеширование мегабайта кусками по килобайту не отпустит GIL ни разу, и масштабирования не будет — при том что «библиотека отпускает GIL» формально правда. Размер куска в таком коде оказывается настройкой параллелизма, и об этом обычно не думают.

Вторая: библиотека уже заняла ядра сама. Для NumPy это записано отдельно:

NumPy itself is normally intentionally limited to a single thread during function calls, however it does support multiple Python threads running at the same time. Note that for performant linear algebra NumPy uses a BLAS backend such as OpenBLAS or MKL, which may use multiple threads that may be controlled by environment variables such as OMP_NUM_THREADS depending on what is used.

перевод

Сам NumPy при вызовах функций обычно намеренно ограничен одним потоком, однако он поддерживает одновременную работу нескольких потоков Python. Учтите, что для производительной линейной алгебры NumPy использует бэкенд BLAS вроде OpenBLAS или MKL, который может использовать несколько потоков, управляемых переменными окружения вроде OMP_NUM_THREADS.

— NumPy — Global state

Четыре потока Python поверх BLAS, который сам берёт четыре потока на ядро, — это переподписка, а не ускорение. Ручка тут не в Python, а в OMP_NUM_THREADS, и крутить её надо до того, как удивляться числам.

Третья, и она не универсальна: не всякая операция отпускает GIL. Документация NumPy оговаривает границу прямо:

Note that operations that do not release the GIL will see no performance gains from use of the threading module, and instead might be better served with multiprocessing. In particular, operations on arrays with dtype=np.object_ do not release the GIL.

перевод

Заметьте, что операции, которые GIL не отпускают, никакого выигрыша от threading не получат, и им может больше подойти multiprocessing. В частности, операции над массивами с dtype=np.object_ GIL не отпускают.

— NumPy — Thread safety

Массив объектов — это массив ссылок на объекты Python, и работать с ними без GIL нельзя по тому самому правилу, с которого статья начиналась. Один dtype решает, будет ли threading иметь смысл.

И четвёртая, которой мы не утверждаем. Полосу памяти как причину сублинейности называют часто, и звучит она правдоподобно. Ни в документации NumPy, ни в C API CPython такого утверждения нет; своего замера у нас тоже нет — на двух ядрах он был бы неубедительным. Поэтому здесь остаётся то, что документировано: порог, переподписка и dtype. Если ваш zlib даёт ×2,04, а не ×2,0, объяснять эту разницу полосой памяти — гипотеза, а не факт.

Свободные потоки: что уже правда

Здесь легко переусердствовать в обе стороны, поэтому — по датам и по документам.

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 (глобальная блокировка интерпретатора останется значением по умолчанию для сборок CPython и загрузок с python.org). То есть уже в 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 (PEP 779: свободная от GIL сборка Python официально поддерживается).

Фазы 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). На момент написания такого PEP нет.

Что происходит прямо сейчас: 3.15 находится в стадии кандидата в релизы (rc1 вышел 4 августа 2026, финал назначен на 1 октября), и изменения там — про инструменты, а не про смену умолчания: PEP 803 добавляет стабильный ABI для свободных сборок (abi3t), у нового профилировщика Tachyon появляется режим --mode gil, а семейство PyGILState_* объявлено мягко устаревшим — без планов удаления и с сохранением работоспособности существующего кода.

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

PYTHON
import sys, sysconfig
 
sys._is_gil_enabled()                        # False в свободной сборке
sysconfig.get_config_var("Py_GIL_DISABLED")  # 1 в свободной сборке, 0 в обычной
sys.abiflags                                 # "t" в свободной сборке

И — обещанный ответ на вопрос из раздела «Что такое GIL буквально»: что делает 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 (свободная от GIL сборка несёт дополнительные накладные расходы при выполнении кода на Python по сравнению со сборкой с GIL, принятой по умолчанию), и дальше — диапазон на наборе pyperformance: from about 1% on macOS aarch64 to 8% on x86-64 Linux systems (примерно от 1 % на macOS aarch64 до 8 % на системах x86-64 Linux).

Мои замеры дали больше. Прежде чем читать проценты, одна оговорка, без которой они соврут: это сравнение двух сборок, а надёжно сравнивать можно только измеренное внутри одного запуска одного интерпретатора. Подтвердить, что обе сборки различаются ровно --disable-gil и ничем больше, здесь нечем. Поэтому числа ниже — наблюдение на одной машине, а не цена свободных потоков вообще.

Что мерялосьС 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», а как про явную очередь, у которой есть настройка. Очередь видна, измеряется тремя строками кода и ведёт себя предсказуемо ровно до границы, которая тоже описана — в комментарии рядом с константой, задающей интервал.

Чем измерено

Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Унести в работу

  • Не отказываться от потоков там, где работа ждёт: на вводе-выводе и в расширениях вроде zlib блокировка отпущена, и потоки помогают.
  • Не обещать переключения каждые пять миллисекунд: настройка задаёт, как часто владельца ПРОСЯТ уступить, а одно длинное неделимое действие растягивает ожидание в десятки раз.
  • Не говорить, что в 3.13 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 мс. На 5 и 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). Такого 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» (примерно от 1 % на macOS aarch64 до 8 % на системах x86-64 Linux) на наборе 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» (Это оставлено будущему PEP).https://peps.python.org/pep-0779/
  8. What's New In Python 3.14Официальная документация. Формулировка «PEP 779: Free-threaded Python is officially supported» (PEP 779: свободная от GIL сборка Python официально поддерживается) — момент, когда сборка перестала считаться экспериментальной.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/