Backend · Python

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вопросы из разбора одним списком. Если на каждый есть ответ своими словами — тему можно считать закрытой.

  1. Что такое GIL?
  2. Почему потоки не ускоряют вычисления?
  3. Нужен ли Lock, если есть GIL?
  4. GIL уже убрали?
По этой теме на платформе

Готовьтесь не вслепую

Возьмите маршрут по своей роли и отрабатывайте ответы на вопросы с AI-разбором.