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

Здесь внутри каждого потока выполняется 10000 вызовов globals().__setitem__('x', globals()['x'] + 1). Это эквивалент чтения текущего x, прибавления 1 и записи обратно — набор действий, а не одна атомарная операция.
Почему результат может быть меньше, а не всегда 40000
- В CPython существует GIL (Global Interpreter Lock), который гарантирует, что в каждый конкретный момент времени выполняется не более один поток байткода интерпретатора. Однако это не делает многосложные операции (как x += 1) атомарными.
- Операция инкремента разбивается на несколько байткодов: чтение значения, выполнение сложения, запись результата. Переключение между потоками может произойти между этими шагами, и два потока могут одновременно прочитать одно и то же старое значение и затем перезаписать его, теряя один инкремент (race condition).
- Поэтому итоговая сумма может быть меньше 40000, и результаты между запусками могут отличаться.
Для наглядности можно посмотреть дизассемблированную версию функции, которая делает похожую операцию:

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

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

- Использовать примитивы multiprocessing (если нужен настоящий параллелизм на нескольких CPU):

- Использовать атомарные расширения на C или сторонние библиотеки, предоставляющие атомарные операции, если требуется высокая производительность без блокировок Python.
Разбор вариантов ответов (кратко)
- Утверждение, что GIL делает x += 1 атомарным — неверно. GIL не гарантирует атомарность составных операций.
- Утверждение, что результат может быть меньше — соответствует реальности: race condition при неатомарном инкременте.
- Идея, что результат всегда кратен 4 — не имеет обоснования: потери инкрементов не обязательно происходят по одному на поток ровно одинаково, итог может быть любым числом в диапазоне [0, 40000], не обязательно кратным 4.
- Утверждение, что будет ошибка из‑за GIL — неверно: GIL не выдаёт ошибку, он лишь ограничивает одновременное выполнение байткодов.
Вывод
GIL не решает проблему согласованного доступа к разделяемым данным на уровне высокоуровневых операций. Если несколько потоков модифицируют общий объект, нужно явное синхронизировать доступ (Lock, RLock, примитивы из multiprocessing или атомарные операции из расширений). В противном случае возможны гонки и потеря инкрементов.