Урок 07 из 12 · 2 минуты чтения

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

Что такое GIL и что он не защищает, когда выбирать потоки, процессы или asyncio, concurrent.futures, ограничения multiprocessing, сборка без GIL.

GIL

GIL — глобальная блокировка интерпретатора CPython: в каждый момент байткод Python выполняет только один поток. Следствия:

  • задачи ввода-вывода потоки ускоряют — на ожидании сети и диска GIL отпускается;
  • вычисления потоки не ускоряют — для них процессы или C-расширения, отпускающие GIL (NumPy).

Три инструмента

Что этоДля чего
threadingпотоки одного процесса, общая памятьожидание ввода-вывода
multiprocessingотдельные процессы со своей памятью и своим GILвычисления на нескольких ядрах
asyncioодин поток, кооперативная многозадачностьтысячи одновременных соединений

Блокировки всё равно нужны

GIL защищает внутренние структуры интерпретатора, а не ваши инварианты. counter += 1 — несколько инструкций байткода: прочитать, сложить, записать. Поток может быть прерван между ними, и обновления теряются.

import threading
lock = threading.Lock()

def inc():
    global counter
    with lock:
        counter += 1

«Проверить и изменить» (if k not in d: d[k] = ...) — тоже гонка. Лучший способ — не делить изменяемое состояние: передавать данные через queue.Queue и использовать неизменяемые структуры.

concurrent.futures

Единый интерфейс для пулов потоков и процессов:

from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=16) as pool:
    futures = [pool.submit(download, url) for url in urls]
    for f in as_completed(futures):
        save(f.result())       # исключение задачи бросится здесь

ThreadPoolExecutor — для ввода-вывода, ProcessPoolExecutor — для вычислений; интерфейс одинаковый. with гарантирует остановку пула и ожидание задач.

Ограничения процессов

Процессы не делят память: аргументы и результаты сериализуются pickle и копируются. Поэтому:

  • лямбды, локальные функции, открытые файлы и соединения передать нельзя;
  • большие данные гонять дорого — лучше передать путь к файлу или использовать shared_memory.

Способ запуска важен. fork копирует процесс родителя — быстро, но опасно, если в родителе уже работали потоки: можно унаследовать захваченную блокировку и зависнуть. spawn запускает чистый интерпретатор и заново импортирует модуль — поэтому нужен if __name__ == "__main__":. В macOS и Windows по умолчанию spawn, в Linux с Python 3.14 — forkserver.

Python без GIL

С Python 3.13 есть отдельная сборка без GIL (python3.13t), в 3.14 она официально поддерживается, но не включена по умолчанию. Потоки выполняют Python-код по-настоящему параллельно, вычисления в потоках ускоряются почти линейно по ядрам. Цена — медленнее однопоточный код, а C-расширения должны объявить совместимость, иначе GIL включится снова. Гонки в вашем коде никуда не денутся — их станет только больше.

Что запомнить

  • GIL мешает вычислениям в потоках, но не вводу-выводу.
  • GIL не защищает ваши данные — нужны Lock или очереди.
  • Вычисления — процессы, ожидание — потоки или asyncio.
  • Процессам всё передаётся через pickle.

Проверка урока

Войдите, чтобы проверить себя: сданные проверки всех уроков курса отмечают его темы пройденными на карте и в ИПР.

Вопросы по теме

Читайте также

Партнёр платформы

Разбор собеседования с sotto

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

Разбор собеседования