GIL: почему потоки не ускоряют вычисления

Главная особенность Python-многопоточности, о которую спотыкаются все новички: GIL.

GIL (Global Interpreter Lock) — глобальная блокировка интерпретатора CPython, разрешающая исполнять байт-код Python только одному потоку в каждый момент времени.

В большинстве языков несколько потоков на многоядерной машине считают параллельно. В CPython (самой распространённой реализации Python) это не так: даже если у вас 8 ядер и 8 потоков, байт-код в любой момент исполняет лишь один из них. Остальные ждут GIL.

Зачем вообще нужен GIL

GIL упрощает управление памятью CPython. Подсчёт ссылок (reference counting), на котором держится сборка мусора, должен быть согласованным; глобальная блокировка делает операции с объектами безопасными почти бесплатно. Платой стала невозможность настоящего параллелизма потоков для чистых вычислений.

Что это значит на практике

Возьмём тяжёлую CPU-задачу — сумму квадратов. Разбив её на два потока, вы НЕ получите ускорения в Python: потоки по очереди держат GIL, а накладные расходы на переключение даже немного замедлят программу.

# чистый CPU-bound расчёт (последовательно) — это и есть «потолок» для потоков
total = sum(i * i for i in range(1, 100001))
print("сумма квадратов:", total)

Вывод:

сумма квадратов: 333338333350000

Два потока, считающие половины этой суммы, в CPython отработают примерно за то же время, что и один: GIL не даёт им считать одновременно.

Таймлайн потоков под GIL

Два CPU-потока, одно ядро задействовано из-за GIL:

  Поток 1: [держит GIL]....         [GIL].... 
  Поток 2:               [держит GIL]....    
  ядро:    ===========занято=================>
  (ускорения нет — параллельного счёта не происходит)

Как работает под капотом

Интерпретатор периодически (по умолчанию примерно каждые 5 миллисекунд) проверяет, не пора ли отдать GIL другому потоку, и переключается. Поэтому потоки в Python всё же чередуются — это настоящая конкурентность, просто не параллелизм. Важная деталь: при операциях ввода-вывода и в C-расширениях (например, в numpy) GIL отпускается — отсюда исключения, которые мы разберём в следующих уроках. Существуют реализации без GIL (например, экспериментальный free-threaded CPython), но классический CPython живёт с ним.

Частые ошибки

  • Ждать ускорения CPU-расчётов от threading. Из-за GIL его не будет — нужен multiprocessing.
  • Думать, что GIL делает код потокобезопасным. Нет: x += 1 всё равно может прерваться между байт-кодами и дать гонку.
  • Считать GIL свойством языка Python. Это деталь конкретной реализации CPython, а не спецификации языка.

Итог

  • GIL разрешает исполнять байт-код только одному потоку за раз.
  • CPU-bound код на потоках в CPython не ускоряется.
  • GIL упрощает управление памятью, но это деталь CPython, а не языка.
  • GIL не отменяет гонок — синхронизация всё ещё нужна.
Проверьте себя
1. Что делает GIL в CPython?
AУскоряет все вычисления вдвое
BРазрешает исполнять байт-код Python только одному потоку в каждый момент
CПолностью запрещает многопоточность
DСоздаёт новые процессы автоматически
2. Почему многопоточный CPU-bound код в CPython не ускоряется?
AПотоки слишком медленные сами по себе
BИз-за GIL потоки не могут исполнять байт-код одновременно
CPython не поддерживает циклы в потоках
DCPU-задачи всегда однопоточные
3. Делает ли GIL операцию x += 1 автоматически потокобезопасной?
AДа, GIL устраняет все гонки
BНет, переключение может произойти между байт-кодами, и гонка возможна
CДа, но только для чисел
DGIL вообще не связан с потоками