Урок по треку Python 8 мин

GIL, потоки и процессы в Python

Что такое GIL, когда помогают потоки, а когда процессы, concurrent.futures, гонки и free-threaded CPython.

«Что такое GIL и как с ним жить» спрашивают почти на каждом Python-собесе от middle и выше. Нужно не только дать определение, но и выбрать инструмент под задачу: потоки, процессы или asyncio.

Что такое GIL

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

Важные уточнения:

  • GIL отпускается при блокирующем вводе-выводе (сеть, диск, time.sleep), поэтому потоки хорошо работают для I/O-bound задач.
  • Многие C-расширения (numpy, hashlib на больших данных, сжатие) отпускают GIL во время тяжёлой работы.
  • Для CPU-bound кода на чистом Python потоки не дают ускорения: работает один поток, остальные ждут, плюс накладные расходы на переключение.
  • GIL не делает ваш код потокобезопасным. Он защищает внутренности интерпретатора, а не ваши инварианты: counter += 1 это несколько байткод-операций, и поток может переключиться посередине.

Free-threaded CPython

PEP 703 добавил сборку CPython без GIL. В 3.13 она появилась как экспериментальная (python3.13t), в 3.14 стала официально поддерживаемой (PEP 779), но по-прежнему не сборкой по умолчанию. В ней потоки по-настоящему параллельны на нескольких ядрах, а однопоточный код немного медленнее; C-расширения должны явно поддерживать этот режим. На собесе достаточно знать, что это есть и что стандартный CPython пока с GIL.

Потоки: threading

from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=20) as pool:
    pages = list(pool.map(fetch, urls))

Подходят для параллельных HTTP-запросов, работы с БД, файлами. Общая память, поэтому общие изменяемые данные защищают threading.Lock, или передают через потокобезопасную queue.Queue.

Процессы: multiprocessing

Каждый процесс это отдельный интерпретатор со своим GIL и своей памятью, поэтому CPU-bound код масштабируется на ядра.

from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor() as pool:
    results = list(pool.map(resize_image, paths))

Цена: запуск процесса дороже потока, данные передаются через сериализацию (pickle), поэтому функция и аргументы должны быть picklable, а гонять большие объекты туда-сюда дорого. На macOS и Windows процессы стартуют методом spawn, поэтому код запуска обязательно под if __name__ == '__main__':. На Linux по умолчанию был fork, а с 3.14 по умолчанию forkserver.

Как выбрать

ЗадачаИнструмент
Много ожидания сети, тысячи соединенийasyncio
Ожидание I/O, синхронные библиотекипотоки (ThreadPoolExecutor)
Тяжёлые вычисления на Pythonпроцессы (ProcessPoolExecutor)
Тяжёлые вычисления в numpy и подобномчасто хватает одного потока, библиотека сама параллелит
Фоновые задачи сервисаочередь задач (Celery и аналоги)

Синхронизация

  • Lock и RLock (повторно входимый тем же потоком).
  • Semaphore ограничивает число одновременных операций.
  • Event, Condition для сигналов между потоками.
  • queue.Queue для схемы «производитель-потребитель».

Взаимная блокировка (deadlock) возникает, когда потоки берут несколько блокировок в разном порядке. Правило: единый порядок захвата и with lock: вместо ручных acquire/release.

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

  • Ожидание ускорения CPU-bound кода от потоков.
  • Мнение «раз есть GIL, гонок не бывает».
  • Передача в процесс лямбды или открытого соединения (не сериализуется).
  • Запуск пула процессов без if __name__ == '__main__'.
  • Слишком много потоков: каждый занимает память под стек.

Что спросят на собесе

  • Что такое GIL, зачем он, когда отпускается.
  • Ускорят ли потоки вычисление и запросы к сети, почему.
  • Потоки, процессы или asyncio: как выбрать.
  • Может ли быть гонка при наличии GIL, пример.
  • Что изменилось с free-threaded сборкой.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.