GIL в Python на собеседовании

Ответ «GIL мешает многопоточности» засчитывают редко: дальше спрашивают, когда он отпускается. Разбираем границу между CPU-bound и I/O-bound задачами.

«GIL мешает многопоточности» — формула верная и почти бесполезная: из неё не следует ни когда потоки всё-таки помогают, ни почему в коде всё равно нужны блокировки. Обе границы описаны в документации одной-двумя строками, и ниже разобраны именно они.

Всё, что сказано о поведении, относится к CPython и к Python 3.13, где это оговорено отдельно.

Что такое GIL и зачем он нужен §

Определение из словаря языка стоит держать близко к дословному, потому что в нём три важные детали сразу: GIL — механизм интерпретатора CPython, который гарантирует, что только один поток исполняет байткод Python в каждый момент времени.

Первая деталь — привязка к реализации. GIL принадлежит CPython: другая реализация вправе устроить всё иначе, и оговорка «в CPython» входит в точный ответ.

Вторая — цель, и она объясняет, почему механизм вообще существует. Блокировка делает объектную модель интерпретатора, включая критичные встроенные типы вроде dict, неявно безопасной при конкурентном доступе. То есть GIL решает задачу разработчиков интерпретатора.

Третья — цена, названная в том же абзаце столь же прямо: блокировка всего интерпретатора упрощает его многопоточность ценой значительной части параллелизма, который дают многопроцессорные машины. Такая формулировка удобна тем, что не требует оценок: это размен, зафиксированный в документации.

Когда GIL отпускается §

Здесь лежит главный факт темы, и он записан в словаре одной фразой: GIL всегда отпускается при вводе-выводе. Из неё выводится вся практическая часть разговора.

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

Это и есть граница, которую обычно называют разделением на I/O-bound и CPU-bound. Её ценность в том, что она выводится из одной строки документации.

Есть и вторая, менее известная половина того же абзаца: расширения, стандартные и сторонние, могут отпускать GIL на вычислительно тяжёлых задачах; словарь приводит в пример сжатие и хеширование. Отсюда важное следствие: «CPU-bound значит потоки бесполезны» — упрощение. Если тяжёлая работа выполняется внутри расширения, которое отпускает блокировку, потоки могут дать выигрыш и на вычислениях.

Для случая, когда вычисления идут на чистом Python, остаются процессы: у каждого свой интерпретатор и, значит, свой GIL.

Почему GIL не отменяет блокировок §

Из определения следует вывод, который на первый взгляд противоречит здравому смыслу: GIL не делает ваш код потокобезопасным. Безопасной он делает объектную модель интерпретатора: встроенные типы не развалятся от одновременного доступа. Инварианты прикладного кода в эту гарантию не входят.

Механизм расхождения проще всего увидеть на составной операции. Увеличение счётчика состоит из чтения, сложения и записи. Каждый из трёх шагов выполняется под блокировкой, но между ними блокировка может перейти к другому потоку: он прочитает то же значение, прибавит и запишет, и одно из двух увеличений потеряется. Список при этом не повредится, словарь не сломается; испортится только ваш инвариант, за который GIL и не отвечал.

Отсюда практический вывод: там, где нужна атомарность последовательности действий, нужна явная блокировка.

Что изменилось с версии 3.13 §

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

Начиная с Python 3.13 GIL можно отключить: для этого существует конфигурация сборки --disable-gil, а собранный так интерпретатор запускается с -X gil=0 либо при установленной переменной окружения PYTHON_GIL=0.

Важно сразу добавить вторую половину, иначе поправка превратится в новую неточность. PEP 703, вводящий эту возможность, формулирует статус-кво прямо: GIL остаётся значением по умолчанию для сборок CPython и для загрузок с python.org. Сборка без GIL остаётся отдельной конфигурацией.

Стоит назвать и цену: накладной расход исполнения однопоточных программ в сборке --disable-gil измеряется примерно в 5–6% по сравнению с обычной сборкой. То есть за параллелизм платят однопоточной производительностью.

Проверьте себя

Сначала проговорите ответ, потом сверьтесь с текстом.

Что такое GIL?

Что проверяют: называете вы механизм с его областью действия или воспроизводите вывод «Python не умеет в многопоточность».

Частая ошибка: сказать, что GIL — часть языка Python. Это механизм конкретной реализации.

Раздел «Что такое GIL и зачем он нужен»
Почему потоки не ускоряют вычисления?

Что проверяют: выводите вы ответ из того, что защищает GIL, или помните вывод отдельно от причины.

Частая ошибка: ответить «потому что потоки в Python ненастоящие». Потоки настоящие, операционные; ограничение не в них, а в том, что исполнять байткод одновременно им не дают.

Раздел «Когда GIL отпускается»
Нужен ли Lock, если есть GIL?

Что проверяют: различаете вы, что именно защищено. Вопрос устроен так, что ответ «не нужен» возможен только при смешении объектной модели интерпретатора и инвариантов прикладного кода.

Частая ошибка: ответить «нет, GIL уже всё синхронизирует». Он синхронизирует доступ к внутренним структурам, а не к смыслу составной операции.

Раздел «Почему GIL не отменяет блокировок»
GIL уже убрали?

Что проверяют: знаете вы актуальное состояние и его границы, или у вас одна из двух крайностей: «убрали» либо «убрать нельзя».

Частая ошибка: ответить, что с 3.13 GIL больше нет. Отключаемость появилась, значением по умолчанию остался GIL.

Раздел «Что изменилось с версии 3.13»

Проверьте эти темы вслух

В тренажёре вы отвечаете голосом, а разбор показывает, где ответ просел.