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, или помните вывод отдельно от причины.
Частая ошибка: ответить «потому что потоки в Python ненастоящие». Потоки настоящие, операционные; ограничение не в них, а в том, что исполнять байткод одновременно им не дают.
Нужен ли Lock, если есть GIL?
Что проверяют: различаете вы, что именно защищено. Вопрос устроен так, что ответ «не нужен» возможен только при смешении объектной модели интерпретатора и инвариантов прикладного кода.
Частая ошибка: ответить «нет, GIL уже всё синхронизирует». Он синхронизирует доступ к внутренним структурам, а не к смыслу составной операции.
GIL уже убрали?
Что проверяют: знаете вы актуальное состояние и его границы, или у вас одна из двух крайностей: «убрали» либо «убрать нельзя».
Частая ошибка: ответить, что с 3.13 GIL больше нет. Отключаемость появилась, значением по умолчанию остался GIL.
Проверьте эти темы вслух
В тренажёре вы отвечаете голосом, а разбор показывает, где ответ просел.