Материал проходит вычитку — возможны неточности.
Урок 103 из 118

Инварианты и singleflight

Биллинг починили правилом: проверка баланса и списание оказались под одним мьютексом, и инвариант «баланс не ниже нуля» перестал падать. Воркеры до этого наперегонки разбирали один документ, и Миша убрал дубли через singleflight.

Главная мысль

логические гонки рождаются из составных операций — check-then-act и инвариантов на нескольких переменных; check-then-act прячут под один мьютекс, а «одно вычисление на ключ» даёт singleflight.

Check-then-act

// сломанный check-then-act: проверка и списание в разных секциях
mu.Lock()
enough := user.Balance >= amount
mu.Unlock()

if enough {
    mu.Lock()
    user.Balance -= amount
    mu.Unlock()
}
// исправленный: проверка и списание в одной секции
mu.Lock()
if user.Balance >= amount {
    user.Balance -= amount
}
mu.Unlock()

Классика: «проверил, потом действуешь». В сломанном варианте проверка и списание — в двух секциях одного мьютекса: между ними может вклиниться другая горутина и изменить баланс, и решение принимается по устаревшему состоянию. Это race condition в чистом виде: детектор молчит (всё синхронизировано), а баланс уходит в минус. Спасение — чтобы проверка и изменение оказались под одним мьютексом, одной критической секцией: тогда между «проверил» и «сделал» никто не вклинится.

Инвариант на двух переменных

var total atomic.Int64
var count atomic.Int64
// обновляем обе: total.Add(x); count.Add(1)
// читатель: avg := total.Load() / count.Load() // ?

Каждая операция атомарна, но пара обновлений — нет. Читатель может поймать момент между двумя Add: total уже вырос, count ещё нет — среднее «прыгнуло». Атомик закрывает data race, но не логическую гонку: два атомарных обновления не образуют один инвариант, и -race промолчит. Лечится мьютексом вокруг пары — он держит инвариант, а не отдельные значения.

Singleflight

Частный случай «двойной работы» из: много горутин одновременно видят «значения нет» и каждая запускает своё дорогое вычисление. Решение — golang.org/x/sync/singleflight: одно вычисление на ключ, остальные ждут его результат.

var g singleflight.Group

func load(key string) (int, error) {
    v, err, _ := g.Do(key, func() (any, error) {
        return expensiveLoad(key)
    })
    return v.(int), err
}

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

В следующем уроке

Вы узнаете:

  • как DoChan и лимиты времени работают в singleflight
  • как инвариант «сумма долей = 100%» устроен
Задание · вопросы

Проверьте себя

Вопрос 1 из 3

Проверка баланса и списание в двух разных секциях одного мьютекса. Что это?

Вопрос 2 из 3

Что делает singleflight?

Вопрос 3 из 3

Два атомика обновляются парой, читаются по отдельности. Что видит читатель?

Итоги

Логические гонки: check-then-act — под один мьютекс; инвариант из нескольких атомиков — под мьютекс; одно вычисление на ключ — singleflight. Инструменты не ловят — только дисциплина.

Наш сайт собирает куки. Чтобы тебе было удобнее им пользоваться. Нажимая на кнопку, ты даёшь разрешение собирать куки