Атомики и модель памяти
Пакет sync/atomic, атомики на уровне CPU и модель памяти Go с happens-before.
6 вопросов
JuniorТеорияОчень частоЧто предоставляет пакет sync/atomic?
Что предоставляет пакет sync/atomic?
sync/atomic предоставляет операции без блокировок над одним машинным словом — Load, Store, Add, Swap и CompareAndSwap — выполняемые неделимо. Они устраняют гонку данных на простых счётчиках и флагах без мьютекса, а типизированные обёртки atomic.Int64/atomic.Value дают к ним безопасный доступ.
Типичные ошибки
- ✗Смешивать атомарный и обычный доступ к одной переменной — от гонки спасает только сплошной атомарный доступ
- ✗Считать, что атомики защищают многошаговый инвариант — они покрывают лишь одно слово
- ✗Забыть, что неатомарное 64-битное поле на 32-битных платформах должно быть выровнено по 8 байтам
Уточняющие вопросы
- →Почему нельзя смешивать атомарный и неатомарный доступ к одной переменной?
- →Что возвращает CompareAndSwap и как его обычно применяют в цикле?
MiddleТеорияЧастоКогда следует использовать атомики вместо мьютекса?
Когда следует использовать атомики вместо мьютекса?
Используйте атомики, когда общее состояние — одно машинное слово — счётчик или флаг, обновляемое одной неделимой операцией. Используйте sync.Mutex, когда нужно держать согласованными несколько значений или выполнять многошаговую критическую секцию. Атомики быстрее, но защищают лишь одну переменную.
Типичные ошибки
- ✗Использовать атомики для обновления нескольких полей, которые должны быть взаимно согласованы
- ✗Брать мьютекс для горячего счётчика в одно слово, где достаточно атомарного Add
- ✗Считать, что атомарное сохранение публикует посторонние переменные — оно упорядочивает только себя
Уточняющие вопросы
- →Как защитить инвариант, охватывающий два счётчика, которые должны меняться вместе?
- →Почему атомарный цикл без блокировок при высокой конкуренции бывает медленнее мьютекса?
MiddleДебаггингЧастоПочему этот count++ в 1000 горутинах не доходит надёжно до 1000 и как это исправить?
Почему этот count++ в 1000 горутинах не доходит надёжно до 1000 и как это исправить?
count++ — это read-modify-write, не атомарно. Много горутин читают одно значение, увеличивают и пишут обратно, поэтому инкременты теряются, а итог недетерминирован — go run -race ловит data race. Исправление — mutex вокруг count++ или atomic.AddInt64(&count, 1) с count типа int64.
Типичные ошибки
- ✗Считать
count++одной атомарной инструкцией, а не read-modify-write - ✗Винить
WaitGroupвместо несинхронизированной общей записи - ✗Думать, что
time.Sleepили сброс кэша исправляет data race
Уточняющие вопросы
- →Почему
atomic.AddInt64дешевле mutex для одного счётчика? - →Что именно наблюдает детектор
-race, чтобы пометить это?
SeniorТеорияИногдаКогда атомарные операции могут оказаться медленнее мьютекса?
Когда атомарные операции могут оказаться медленнее мьютекса?
Обычно atomic быстрее мьютекса на одном слове, но при высокой конкуренции, когда множество ядер долбят одну переменную, каждая операция read-modify-write инвалидирует эту строку кэша на всех ядрах, и она «летает» между процессорами (трафик когерентности MESI), а пропускная способность рушится — выигрывает mutex, паркующий ожидающих. Atomic проигрывает и тогда, когда несколько переменных должны быть согласованы: это требует множества операций вместо одной секции.
Типичные ошибки
- ✗Считать atomic всегда быстрее, игнорируя «летание» строки кэша при конкуренции
- ✗Координировать несколько переменных множеством atomic-операций вместо одной критической секции
- ✗Думать, что цикл без блокировок масштабируется линейно, когда одну переменную бьют много ядер
Уточняющие вопросы
- →Почему трафик когерентности кэша (MESI) замедляет конкурентный atomic-цикл?
- →Почему mutex, паркующий ожидающих, обгоняет atomic по пропускной способности при высокой конкуренции?
SeniorТеорияРедкоКак атомарные операции реализованы на уровне инструкций CPU?
Как атомарные операции реализованы на уровне инструкций CPU?
На x86 атомики компилируются в инструкции с префиксом LOCK (LOCK XADD, LOCK CMPXCHG), удерживающие кэш-линию эксклюзивно на время доступа. ARM использует пару LL/SC (load-linked / store-conditional), повторяющую попытку при конфликте. Барьеры памяти задают порядок, чтобы другие ядра увидели результат.
Типичные ошибки
- ✗Думать, что выровненный MOV атомарен между ядрами без префикса LOCK или барьера
- ✗Считать, что атомики отключают прерывания или входят в ядро, а не блокируют кэш-линию
- ✗Полагать, что CAS всегда успешен — LL/SC и CMPXCHG могут провалиться и требуют цикла повтора
Уточняющие вопросы
- →Почему конкурентное атомарное сложение плохо масштабируется на множество ядер?
- →Что такое проблема ABA и как она влияет на цикл CompareAndSwap?
SeniorТеорияРедкоЧто гарантирует модель памяти Go относительно порядка happens-before?
Что гарантирует модель памяти Go относительно порядка happens-before?
Модель памяти Go определяет happens-before: если событие A happens-before B, записи A видны в B. Отправка в канал happens-before соответствующего приёма; Unlock мьютекса happens-before следующего Lock. Без такого ребра синхронизации у goroutine вообще нет гарантий порядка.
Типичные ошибки
- ✗Считать, что записи видны между goroutine без явного ребра синхронизации
- ✗Думать, что порядок инструкций в исходнике сохраняется так, как его видит другая goroutine
- ✗Путать направление — отправка happens-before приёма, а не наоборот
Уточняющие вопросы
- →Какое ребро happens-before устанавливает закрытие канала для его получателей?
- →Почему гонка данных в Go — неопределённое поведение, а не просто несвежее чтение?