Гонки данных при инкременте в потоках Python
@python_quizРазберем этот квиз
Коротко: два потока выполняют по 1000 инкрементов глобальной переменной x. Инкремент не атомарен, поэтому итог часто меньше ожидаемых 2000 — происходит гонка данных.
Условие (код квиза)

Код пытается выполнить 2000 инкрементов, но результат обычно не стабильно равен 2000.
Почему инкремент может "теряться"
Инкремент (чтение значения, увеличение, запись) в Python представляет собой несколько отдельных операций:
- загрузка текущего значения,
- вычисление нового значения (создание нового int),
- запись нового значения в глобальную переменную.
Между этими шагами планировщик потоков может переключить выполнение на другой поток. Тогда оба потока могут прочитать одно и то же старое значение и записать одно и то же новое — один инкремент "теряется". Это и есть классическая гонка данных.
Важно: наличие GIL (Global Interpreter Lock) в CPython не делает такие составные операции атомарными. GIL не предотвращает переключения между байт-кодами, поэтому последовательность байт-кодов, реализующих инкремент, всё ещё может быть прервана.
Разбор вариантов ответов (логика)
- "Всегда 2000, потоковость не влияет"
Неверно: потоковость влияет, потому что инкремент не атомарен, возможны потери.
- "Всегда 0, x не глобальный в потоках"
Неверно: глобальная переменная доступна в потоках — поток может изменить global, результат не останется 0.
- "Может быть меньше 2000 из-за гонки данных"
Именно это объясняет поведение: без синхронизации инкременты могут теряться, итог часто < 2000 (иногда — 2000, но не гарантированно).
- "Ошибка: поток не может менять глобальные переменные"
Неверно: потоки могут менять глобальные переменные, ошибок не возникает.
Демонстрация исправления: блокировки
Самый прямой способ — синхронизировать доступ с помощью threading.Lock.
Пример исправленного варианта:

Использование lock обеспечивает, что чтение/изменение/запись происходят как одна неделимая секция.
Альтернативы и примечания
- multiprocessing: для реального параллелизма (обход GIL) можно использовать multiprocessing и разделяемые значения (multiprocessing.Value/Array) с блокировкой.
- атомарные операции: стандартной атомарной операции для int в CPython нет; сторонние расширения (C-расширения, atomic-примитивы) или библиотечные структуры могут дать атомарность.
- производительность: блокировка корректна, но может снизить параллелизм. Для задач с высокой конкуренцией стоит подумать о дизайне: шардирование счётчика, варианты со сбором локальных сумм в потоках и затем объединение, или использование очередей и одного рабочего для агрегации.
Краткие выводы
- Инкремент в Python — не атомарная операция; в многопоточном коде без синхронизации возможны потерянные обновления.
- GIL не даёт гарантии атомарности на уровне выражений, только предотвращает одновременное исполнение байт-кода разными потоками, но переключения между байт-кодами возможны.
- Решение: синхронизация (Lock), использование multiprocessing или специализированных атомарных примитивов в зависимости от задачи.