Какие проблемы возникают при инкременте глобальной переменной из нескольких goroutine?
Что проверяют
Знаете ли вы, что counter++ не атомарен и что гонка данных в Go это ошибка программы, а не просто неточный счётчик. Следом обычно просят выбрать между мьютексом, атомиком и каналом и показать, как гонку обнаружить.
Как отвечать
Инкремент это три шага: прочитать, прибавить единицу, записать. Две горутины читают одно и то же значение, обе записывают n+1, и одно обновление теряется. Запустите 1000 горутин, и итог будет меньше 1000, причём каждый раз разный.
По модели памяти Go программа с гонкой некорректна: компилятор вправе держать значение в регистре и не перечитывать его, поэтому цикл ожидания флага без синхронизации может вообще не увидеть изменение. С map ещё жёстче: одновременную запись рантайм ловит и роняет процесс с fatal error: concurrent map writes, и recover это не перехватит.
Как чинить:
sync/atomic: для простого счётчика лучший вариант,atomic.Int64с методомAdd(1)илиatomic.AddInt64(&n, 1). Типизированные атомики появились в Go 1.19 и не дают случайно прочитать значение в обход атомика;sync.Mutex: когда под одной блокировкой меняются несколько связанных полей. Держите блокировку как можно короче,defer mu.Unlock()сразу послеLock();- канал или горутина-владелец: состояние меняет только одна горутина, остальные шлют ей сообщения. Хорошо для сложной логики, для счётчика это лишние накладные расходы.
Обнаружение: go test -race или go run -race. Детектор находит только те гонки, что реально случились при прогоне, поэтому тесты должны давать конкурентную нагрузку.
Где спотыкаются
Думают, что при GOMAXPROCS=1 гонки нет. Планировщик вытесняет горутины асинхронно, и по модели памяти код всё равно некорректен.
Смешивают доступы: пишут через atomic.AddInt64, а читают обычным n. Это тоже гонка, читать нужно через atomic.LoadInt64.
Забывают дождаться горутин. Без sync.WaitGroup функция main завершается раньше, счётчик показывает ноль или случайное число, а кандидат ищет проблему в синхронизации.