Инварианты и 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%» устроен
Проверьте себя
Проверка баланса и списание в двух разных секциях одного мьютекса. Что это?
Что делает singleflight?
Два атомика обновляются парой, читаются по отдельности. Что видит читатель?
правильных ответов
Логические гонки: check-then-act — под один мьютекс; инвариант из нескольких атомиков — под мьютекс; одно вычисление на ключ — singleflight. Инструменты не ловят — только дисциплина.
10 000 ₽ · 60 минут · онлайн