Состояние гонки (race condition)

Самый коварный класс багов: результат зависит от того, в каком порядке выполнились потоки.

Состояние гонки (race condition) — это ситуация, когда корректность программы зависит от относительного порядка или времени выполнения нескольких потоков, а этот порядок не гарантирован.

Гонки возникают там, где несколько потоков читают и пишут одни и те же данные без согласования. Классический пример — общий счётчик, который инкрементируют два потока.

Почему x += 1 не атомарна

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

Поток A         Поток B
read  counter=5
                read  counter=5
add   -> 6
                add   -> 6
write counter=6
                write counter=6

Два инкремента, а итог 6, а не 7. Один потерян!

Это и есть гонка: оба потока прочитали 5, оба записали 6, и одно увеличение «потерялось». Результат зависит от точного момента переключения — поэтому баг плавающий и трудно воспроизводимый.

Демонстрация «потерянного обновления»

Смоделируем перемежающиеся шаги вручную, без настоящих потоков, чтобы показать, как теряется обновление:

counter = 5

# оба потока прочитали ДО записи
a_read = counter
b_read = counter

# оба прибавили на своей копии
a_new = a_read + 1
b_new = b_read + 1

# оба записали
counter = a_new
counter = b_new

print("Ожидали 7, получили:", counter)

Вывод:

Ожидали 7, получили: 6

Как работает под капотом

Процессор и компилятор могут переупорядочивать инструкции и кэшировать значения в регистрах. Даже простое чтение-изменение-запись разбивается на несколько машинных команд, и вытесняющий планировщик способен вклиниться между ними. Чем больше потоков и общих данных, тем выше шанс попасть в «неудачное» чередование. Гонки особенно опасны тем, что в тестах могут не проявляться, а в продакшене под нагрузкой — внезапно сломать данные.

Частые ошибки

  • Считать любую короткую операцию атомарной. +=, append в общий список, проверка-и-действие — всё это уязвимо.
  • «У меня всё работает». Гонка может проявляться раз в тысячу запусков; отсутствие падения не доказывает корректность.
  • Защищать только запись, забывая про чтение. Несогласованное чтение тоже даёт мусор.

Итог

  • Гонка — зависимость результата от порядка выполнения потоков.
  • x += 1 — это read-modify-write, не одна атомарная операция.
  • Баги гонок плавающие и плохо воспроизводимые.
  • Любой общий изменяемый доступ нужно согласовывать.
Проверьте себя
1. Почему операция counter += 1 небезопасна между потоками?
AОна слишком медленная
BЭто последовательность чтение-изменение-запись, и переключение может вклиниться посередине
CPython запрещает += в потоках
DОна использует слишком много памяти
2. Чем особенно опасны состояния гонки?
AОни всегда роняют программу сразу
BОни проявляются недетерминированно и плохо воспроизводятся
CОни видны только в комментариях кода
DОни замедляют компиляцию