Приходилось ли работать с атомарными операциями в Go
Что проверяют
Понимаете ли вы, зачем нужны атомарные операции, и умеете ли выбирать между ними и мьютексом. Заодно проверяют знание того, что гонка это не только одновременная запись: обычное чтение рядом с атомарной записью тоже гонка.
Как отвечать
Атомарная операция выполняется целиком, её нельзя увидеть наполовину сделанной. В Go они живут в пакете sync/atomic: Add, Load, Store, Swap, CompareAndSwap. Современный и более безопасный способ это типы вместо голых функций:
var hits atomic.Int64
hits.Add(1)
fmt.Println(hits.Load())Где они уместны: счётчики метрик и обработанных сообщений, флаги завершения, публикация указателя на неизменяемую структуру через atomic.Pointer, когда читателей много, а обновление редкое. Где нет: как только нужно согласованно менять несколько полей или держать инвариант между ними, берите sync.Mutex, код выйдет и правильнее, и понятнее.
Хорошо добавить, что типы вроде atomic.Int64 снимают старую проблему выравнивания 64-битных полей на 32-битных платформах, и что любую догадку о гонках проверяют детектором: go test -race.
Где спотыкаются
Пишут атомарно, а читают обычным обращением к переменной. Это по-прежнему гонка: значение может оказаться устаревшим или прочитанным частично. Читать нужно через Load, и детектор гонок такое ловит сразу.
Считают атомарные операции всегда более быстрыми. На высокой конкуренции цикл CompareAndSwap крутится вхолостую, а счётчик, лежащий в одной кэш-линии с соседними данными, бьёт по производительности всех ядер. Мьютекс на такой нагрузке нередко выигрывает.
Собирают составную операцию из нескольких атомарных. Прочитать значение через Load, сравнить и потом изменить через Add уже не атомарно: между действиями вклинится другая горутина. Нужен CompareAndSwap в цикле либо мьютекс на весь блок.