Состояние гонки и критическая секция

Почему два потока, увеличивающих счётчик, могут «потерять» часть прибавлений.

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

Откуда берётся гонка

Безобидная на вид операция balance = balance + 50 на самом деле состоит из трёх шагов:

  1. прочитать текущее значение balance в регистр;
  2. прибавить 50 к регистру;
  3. записать результат обратно в balance.

Если два потока выполняют это одновременно и их шаги чередуются неудачно, прибавление может потеряться. Оба прочитают одно и то же старое значение, оба прибавят, оба запишут — и одно из двух пополнений исчезнет.

Наглядная симуляция гонки

Смоделируем чередование инструкций двух потоков над общим балансом. Сначала корректный последовательный порядок, потом — порядок с гонкой.

def run(interleaving):
    balance = 100
    regs = {"A": None, "B": None}
    for thread, op in interleaving:
        if op == "read":
            regs[thread] = balance      # прочитать общий balance
        elif op == "add":
            regs[thread] += 50          # прибавить в своём регистре
        elif op == "write":
            balance = regs[thread]      # записать обратно
    return balance

# Корректно: поток A целиком, потом поток B
good = [("A","read"),("A","add"),("A","write"),
        ("B","read"),("B","add"),("B","write")]
print(f"Последовательно: итог = {run(good)} (ожидаем 200)")

# Гонка: оба прочитали ДО того, как кто-то записал
bad = [("A","read"),("B","read"),
       ("A","add"),("B","add"),
       ("A","write"),("B","write")]
print(f"С гонкой:        итог = {run(bad)} (одно пополнение потеряно!)")

Вывод:

Последовательно: итог = 200 (ожидаем 200)
С гонкой:        итог = 150 (одно пополнение потеряно!)

Оба варианта используют один и тот же код, но разный порядок выполнения даёт разный результат. Именно это и есть гонка: значение 150 вместо 200 — потерянное обновление.

Критическая секция

Критическая секция — это участок кода, который обращается к общим данным и не должен выполняться двумя потоками одновременно. В нашем примере критическая секция — три шага «прочитать, прибавить, записать».

Решение проблемы гонок — обеспечить взаимное исключение (mutual exclusion): в критической секции в каждый момент должен находиться не более чем один поток. Пока поток A внутри, поток B ждёт у входа.

Три требования к решению

Хорошее решение задачи критической секции обязано удовлетворять трём условиям:

  • Взаимное исключение. Не более одного потока в критической секции одновременно.
  • Прогресс. Если секция свободна, один из желающих войти должен войти (никто не блокируется зря).
  • Ограниченное ожидание. Поток не может ждать входа бесконечно — нет голодания.

Почему нельзя «просто быть аккуратным»

Новички думают, что гонки — редкость и «у меня не воспроизводится». Проблема в том, что планировщик переключает потоки в непредсказуемые моменты. Баг может проявляться раз в миллион запусков — и именно в проде под нагрузкой. Поэтому общие данные защищают всегда примитивами синхронизации, а не надеждой на удачу. Эти примитивы — мьютексы и семафоры — тема следующего урока.

Итог

  • Состояние гонки — зависимость результата от порядка выполнения потоков над общими данными.
  • Даже x = x + 1 неатомарна: чтение, изменение, запись могут чередоваться.
  • Критическая секция — код, работающий с общими данными, требует взаимного исключения.
  • Корректное решение даёт взаимное исключение, прогресс и ограниченное ожидание.
  • Гонки непредсказуемы — общие данные нужно защищать всегда.
Проверьте себя
1. Почему операция balance = balance + 50 может потерять обновление при гонке?
AПотому что 50 — слишком большое число
BОна состоит из трёх шагов (чтение, изменение, запись), которые могут неудачно чередоваться
CПотому что balance хранится на диске
DПотому что сложение всегда атомарно
2. Что такое критическая секция?
AСамый медленный участок программы
BУчасток кода, обращающийся к общим данным, который не должны выполнять два потока одновременно
CРаздел памяти только для чтения
DКод, выполняемый в режиме ядра
3. Какое из требований к решению критической секции означает отсутствие голодания?
AВзаимное исключение
BПрогресс
CОграниченное ожидание
DАтомарность
Поддержать проект