Гонки данных при инкременте в потоках Python

Гонки данных при инкременте в потоках Python

@python_quiz

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

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

Условие (код квиза)

python

Код пытается выполнить 2000 инкрементов, но результат обычно не стабильно равен 2000.

Почему инкремент может "теряться"

Инкремент (чтение значения, увеличение, запись) в Python представляет собой несколько отдельных операций:

  • загрузка текущего значения,
  • вычисление нового значения (создание нового int),
  • запись нового значения в глобальную переменную.

Между этими шагами планировщик потоков может переключить выполнение на другой поток. Тогда оба потока могут прочитать одно и то же старое значение и записать одно и то же новое — один инкремент "теряется". Это и есть классическая гонка данных.

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

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

  • "Всегда 2000, потоковость не влияет"

Неверно: потоковость влияет, потому что инкремент не атомарен, возможны потери.

  • "Всегда 0, x не глобальный в потоках"

Неверно: глобальная переменная доступна в потоках — поток может изменить global, результат не останется 0.

  • "Может быть меньше 2000 из-за гонки данных"

Именно это объясняет поведение: без синхронизации инкременты могут теряться, итог часто < 2000 (иногда — 2000, но не гарантированно).

  • "Ошибка: поток не может менять глобальные переменные"

Неверно: потоки могут менять глобальные переменные, ошибок не возникает.

Демонстрация исправления: блокировки

Самый прямой способ — синхронизировать доступ с помощью threading.Lock.

Пример исправленного варианта:

python

Использование lock обеспечивает, что чтение/изменение/запись происходят как одна неделимая секция.

Альтернативы и примечания

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

Краткие выводы

  • Инкремент в Python — не атомарная операция; в многопоточном коде без синхронизации возможны потерянные обновления.
  • GIL не даёт гарантии атомарности на уровне выражений, только предотвращает одновременное исполнение байт-кода разными потоками, но переключения между байт-кодами возможны.
  • Решение: синхронизация (Lock), использование multiprocessing или специализированных атомарных примитивов в зависимости от задачи.

Report Page