sync/atomic делает отдельную операцию неделимой. Это полезно для счётчиков и флагов, но не превращает любую последовательность действий в транзакцию.
Когда атомик проще мьютекса
var requests atomic.Int64
requests.Add(1)
var stopped atomic.Bool
stopped.Store(true)
Атомик подходит для:
- счётчика запросов или ошибок;
- флага остановки;
- выдачи следующего ID;
- чтения текущего значения без составной логики.
Мьютекс нужен, если проверка и изменение зависят от состояния:
mu.Lock()
if balance >= price {
balance -= price
}
mu.Unlock()
Вариант if balance >= price { balance -= price } через Load и Add содержит гонку: между ними другой списатель изменит баланс.
CompareAndSwap
CAS записывает новое значение только если старое не изменилось:
for {
current := balance.Load()
if current < price {
return ErrInsufficientFunds
}
if balance.CompareAndSwap(current, current-price) {
return nil
}
}
Здесь проверка и обновление атомарны. При неудаче цикл читает состояние заново. Такой код часто сложнее мьютекса и подвержен contention: на горячем значении мьютекс может быть быстрее и понятнее.
CAS полезен для небольшой конечной машины состояний и lock-free счётчиков, но требует тестов на конкурентные изменения.
Два значения снова не одна операция
type Account struct {
balance atomic.Int64
frozen atomic.Bool
}
func (a *Account) Withdraw(amount int64) error {
if a.frozen.Load() {
return ErrFrozen
}
a.balance.Add(-amount)
return nil
}
frozen и balance атомарны по отдельности, но Withdraw не атомарен целиком: заморозка может появиться между проверкой и списанием. Связанные поля меняют под одним мьютексом или упаковывают в один объект состояния.
Проблема ABA
CAS видит не версию объекта, а текущее значение. Последовательность A -> B -> A для него неотличима от «ничего не изменилось». Если меняется владелец, права или состояние объекта, важна не просто сумма, а версия.
Решения:
- хранить
value + versionи сравнивать обе части; - пересоздавать состояние и менять указатель на него;
- использовать мьютекс, если lock-free сложность не оправдана.
ABA не бывает у простого монотонного счётчика, потому что он не возвращается к старому значению.
Выравнивание и копирование
64-битные атомарные операции требуют правильного выравнивания адреса, особенно на 32-битных архитектурах. Поле атомарной переменной лучше ставить первым в структуре или использовать выделенную переменную.
После первого использования структуру с атомарной переменной не копируют. Значение noCopy в исходниках atomic напоминает об этом, а go vet находит часть таких копирований.
Коротко
- Атомик заменяет мьютекс для простого значения, не для бизнес-операции.
- CAS проверяет и меняет значение атомарно, но требует цикла и тестов на contention.
- Несколько атомарных полей не образуют атомарное состояние.
- ABA решают версией состояния или мьютексом.
- Атомарные поля не копируют после первого использования.