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 не отменяет гонок — синхронизация всё ещё нужна.