Вопрос проверяет понимание механизма GIL в CPython и того, как он ограничивает использование многопоточности для CPU-ёмких задач.
GIL (Global Interpreter Lock) — это глобальная блокировка интерпретатора в CPython, которая позволяет одновременно исполняться только одному потоку Python-байткода. Из-за этого многопоточность в Python не ускоряет CPU-ёмкие задачи, так как потоки вынуждены по очереди получать GIL и выполнять код. Однако для I/O-операций потоки всё ещё полезны: когда поток блокируется на ввод-выводе, GIL освобождается, и другой поток может выполняться. Для обхода ограничений GIL используют многопроцессность (multiprocessing), нативные расширения на C или асинхронность (asyncio) для I/O-сценариев.
Чтобы понимать поведение многопоточности в Python, важно знать, как устроен GIL.
Определение:
GIL (Global Interpreter Lock) — это глобальная блокировка в интерпретаторе CPython, которая гарантирует, что в каждый момент времени только один поток выполняет Python-байткод.
Причины существования GIL:
Упрощение реализации интерпретатора
Управление памятью через reference counting проще реализовать, когда есть один глобальный lock.
Меньше сложных тонких ошибок в С-коде интерпретатора.
Производительность для однопоточных сценариев
В однопоточном коде GIL почти не мешает и позволяет не ставить замки на каждую операцию с объектами.
Для задач, нагружающих CPU (численные вычисления, большая обработка в Python-циклах):
Все потоки борются за один GIL.
В каждый момент времени Python-байткод выполняет только один поток.
Планировщик периодически переключает GIL между потоками (по таймеру или при некоторых операциях), но это не даёт параллельного выполнения на нескольких ядрах.
В результате:
нет масштабирования по числу ядер;
может быть даже медленнее из-за overhead переключения.
Пример: два потока, которые просто считают числа, не будут работать вдвое быстрее на двух ядрах.
Для задач ввода-вывода (HTTP-запросы, чтение/запись файлов, работа с БД):
Когда поток блокируется на I/O, он освобождает GIL.
В это время другой поток может выполнять Python-код.
В итоге многопоточность даёт выигрыш, так как CPU не простаивает во время ожидания I/O.
Это делает threading полезным для нагрузочного тестирования API, параллельной загрузки файлов и т.п.
Модуль multiprocessing запускает несколько процессов, у каждого свой интерпретатор и свой GIL.
Python
from multiprocessing import Pool
def cpu_heavy(x):
# тяжелые вычисления
return x * x
if __name__ == "__main__":
with Pool(processes=4) as pool:
results = pool.map(cpu_heavy, range(1_000_000))
Процессы могут реально использовать разные ядра CPU.
Нужно учитывать накладные расходы на межпроцессное взаимодействие и сериализацию данных.
Многие библиотеки на C/C++ (NumPy, некоторые ML-библиотеки) временно освобождают GIL внутри своих вычислительных участков.
Это позволяет им использовать несколько ядер, несмотря на GIL.
asyncio) для I/Oasyncio не обходит GIL для CPU, но даёт эффективную обработку большого числа I/O-задач в одном потоке.
Асинхронность комбинируют с ProcessPoolExecutor для тяжёлых вычислений.
Если задача CPU-bound:
Не рассчитывать на ускорение через threading.
Использовать multiprocessing или библиотеки, которые освобождают GIL.
Возможен перенос горячих участков на C/C++/Rust.
Если задача I/O-bound:
Можно использовать threading, concurrent.futures.ThreadPoolExecutor или asyncio.
GIL почти не мешает, так как основной тормоз — ожидание I/O.
При проектировании:
Понимать, какой тип нагрузки преобладает.
Не забывать про примитивы синхронизации — GIL не защищает ваши данные на уровне бизнес-логики.
GIL в CPython ограничивает реальный параллелизм потоков для CPU-ёмких задач, но почти не мешает I/O-bound задачам. Для вычислительных задач лучше использовать процессы или нативные библиотеки, а для ввода-вывода — многопоточность или asyncio.