Инкремент в потоках Python и роль GIL

Инкремент в потоках Python и роль GIL

@python_quiz

Разберем этот квиз

Коротко: приведённый код запускает четыре потока, каждый из которых выполняет 10000 инкрементов глобальной переменной. Интуитивно ожидается 40000, но на практике результат может быть меньше — из‑за того, что операция инкремента не атомарна в CPython, и между её составными байткодами может произойти переключение потоков.

Код из квиза — что он делает

Оригинальный однострочник (расхождён для читаемости):

python

Здесь внутри каждого потока выполняется 10000 вызовов globals().__setitem__('x', globals()['x'] + 1). Это эквивалент чтения текущего x, прибавления 1 и записи обратно — набор действий, а не одна атомарная операция.

Почему результат может быть меньше, а не всегда 40000

  • В CPython существует GIL (Global Interpreter Lock), который гарантирует, что в каждый конкретный момент времени выполняется не более один поток байткода интерпретатора. Однако это не делает многосложные операции (как x += 1) атомарными.
  • Операция инкремента разбивается на несколько байткодов: чтение значения, выполнение сложения, запись результата. Переключение между потоками может произойти между этими шагами, и два потока могут одновременно прочитать одно и то же старое значение и затем перезаписать его, теряя один инкремент (race condition).
  • Поэтому итоговая сумма может быть меньше 40000, и результаты между запусками могут отличаться.

Для наглядности можно посмотреть дизассемблированную версию функции, которая делает похожую операцию:

python

Вы увидите несколько байткодов (вызов globals(), загрузка значения, BINARY_ADD, запись), что подчёркивает неатомарность.

Демонстрация: как поведение варьируется

Пример запуска нескольких испытаний — типично результаты будут отличаться:

python

Вы увидите, что некоторые запуски дают 40000, но многие — меньше (например 39873, 39912 и т.п.). Это случайные потери инкрементов.

Как правильно синхронизировать — варианты решений

  1. Использовать Lock:
python
  1. Использовать примитивы multiprocessing (если нужен настоящий параллелизм на нескольких CPU):
python
  1. Использовать атомарные расширения на C или сторонние библиотеки, предоставляющие атомарные операции, если требуется высокая производительность без блокировок Python.

Разбор вариантов ответов (кратко)

  • Утверждение, что GIL делает x += 1 атомарным — неверно. GIL не гарантирует атомарность составных операций.
  • Утверждение, что результат может быть меньше — соответствует реальности: race condition при неатомарном инкременте.
  • Идея, что результат всегда кратен 4 — не имеет обоснования: потери инкрементов не обязательно происходят по одному на поток ровно одинаково, итог может быть любым числом в диапазоне [0, 40000], не обязательно кратным 4.
  • Утверждение, что будет ошибка из‑за GIL — неверно: GIL не выдаёт ошибку, он лишь ограничивает одновременное выполнение байткодов.

Вывод

GIL не решает проблему согласованного доступа к разделяемым данным на уровне высокоуровневых операций. Если несколько потоков модифицируют общий объект, нужно явное синхронизировать доступ (Lock, RLock, примитивы из multiprocessing или атомарные операции из расширений). В противном случае возможны гонки и потеря инкрементов.

Report Page