Атомики и модель памяти
sync/atomic выглядит как самый дешёвый способ синхронизации — одна неделимая операция вместо Lock/Unlock. И действительно, защитить счётчик, флаг или указатель атомиком быстрее и проще, чем мьютексом. Но за этой простотой прячется вся модель памяти Go, и именно её проверяют на серьёзном интервью, а не синтаксис atomic.AddInt64.
Острых граней здесь две. Первая — атомик неделим только для своей переменной: двумя атомиками нельзя защитить инвариант, связывающий два значения, и смешанный атомарный с обычным доступ к одной переменной остаётся гонкой данных. Вторая, более глубокая — модель памяти: без явного ребра happens-before одна goroutine не обязана видеть записи другой и даже может увидеть их в другом порядке. Гонка данных в Go — это не «иногда несвежее чтение», а неопределённое поведение. Эта тема разбирает атомики по слоям — от пяти операций пакета sync/atomic до того, как LOCK-префикс и барьер памяти превращают инструкцию процессора в ребро синхронизации.
Карта темы
- Пакет sync/atomic — операции без блокировок над одним машинным словом:
Load,Store,Add,Swap,CompareAndSwap, типизированные обёрткиatomic.Int64/atomic.Valueи CAS-цикл. - Атомики на уровне CPU —
LOCK-префикс на x86, пара load-linked/store-conditional на ARM, барьеры памяти и почему конкурентный атомик плохо масштабируется. - Модель памяти Go — правила happens-before, рёбра синхронизации каналов, мьютексов и
sync.Once, гонка данных как неопределённое поведение. - Атомики против мьютекса — когда хватает одной неделимой операции над одним словом, а когда нужна полноценная критическая секция под мьютексом.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Смешать атомарный и обычный доступ к одной переменной | Гонка данных остаётся; неопределённое поведение, спасает только сплошной атомарный доступ |
| Защищать атомиками многошаговый инвариант или пару значений | Атомик покрывает лишь одно слово; промежуточное состояние видно другим goroutine |
| Считать, что атомарное сохранение публикует и соседние переменные | Атомик упорядочивает только сам себя, не соседние записи |
Полагать, что выровненный MOV атомарен между ядрами сам по себе | Без LOCK-префикса и барьера другие ядра не увидят результат согласованно |
| Считать, что атомик делает системный вызов или блокирует глобальный мьютекс | Это инструкция CPU без перехода в ядро; затронута лишь одна кэш-линия |
| Думать, что атомик всегда быстрее мьютекса | При высокой конкуренции CAS-цикл крутится вхолостую и проигрывает мьютексу — нужен бенчмарк |
| Считать, что записи видны между goroutine без ребра синхронизации | Несвежие чтения или гонка — модель памяти не даёт порядка без happens-before |
| Перепутать направление ребра — «приём happens-before отправки» | Неверный вывод о порядке планирования читателя и писателя |
| Полагаться на CAS без цикла повтора | CAS может не сработать, если значение успели изменить; нужен цикл «прочитать — посчитать — записать» |
Значение для собеседований
Атомики и модель памяти — то, что отделяет кандидата, умеющего рассуждать о корректности конкурентного кода, от того, кто заучил go func(){}(). Каналы и мьютекс обычно знают; здесь же интервьюер проверяет, есть ли у вас рабочая ментальная модель видимости записей между goroutine.
Что обычно проверяют:
- Что предоставляет
sync/atomicи почему атомик защищает лишь одно машинное слово. - Почему смешанный атомарный и обычный доступ к переменной остаётся гонкой данных.
- Как атомарная операция реализована на уровне CPU —
LOCK-префикс, LL/SC, барьеры памяти. - Почему конкурентный атомик плохо масштабируется и когда мьютекс быстрее.
- Что такое happens-before и какие рёбра синхронизации его устанавливают.
- Что такое гонка данных в Go и почему это неопределённое поведение, а не просто несвежее чтение.
Типичный неверный ответ: «атомики — это просто быстрый мьютекс, ими можно защитить что угодно». Это запускает разбор того, что атомик неделимо обновляет ровно одно слово, а согласованность нескольких значений или многошаговая критическая секция требуют sync.Mutex.