Что такое гонка данных (data race) в Go
Что проверяют
Знаете ли вы определение из модели памяти Go, а не бытовое «два потока лезут в одну переменную». Почти всегда следом идёт второй вопрос: чем гонка данных отличается от гонки состояний и как вы её ловите.
Как отвечать
Гонка данных это два обращения к одной ячейке памяти из разных горутин, не упорядоченные отношением «происходит раньше», причём хотя бы одно из них на запись. Два параллельных чтения гонкой не являются.
Важно, что последствие не сводится к неверному числу. Такая программа объявлена имеющей неопределённое поведение: запись интерфейса или среза не атомарна, и читатель может увидеть указатель от одного значения вместе с типом от другого, а конкурентная запись в map аварийно завершает процесс.
Чем чинят, по возрастанию цены:
- владение: состояние принадлежит одной горутине, остальные общаются с ней через канал;
sync.MutexиRWMutexвокруг всех обращений, включая чтения;sync/atomicдля счётчиков и флагов,sync.Onceдля ленивой инициализации.
Ловится детектором гонок: go test -race и сборка с тем же флагом. Он динамический и видит только те гонки, которые реально случились на исполненном пути, поэтому его включают в прогон тестов постоянно, а не разово. Замедление в несколько раз и рост потребления памяти это нормальная цена.
Где спотыкаются
Путают гонку данных с логической гонкой. Программа, где каждое отдельное обращение атомарно, всё равно теряет обновления, если между чтением и записью успевает вклиниться другая горутина. Проверка и действие должны быть либо под одной блокировкой, либо одной операцией сравнения с обменом.
Закрывают мьютексом запись и оставляют чтение голым. Читателю нужен свой захват блокировки, иначе он видит наполовину обновлённую структуру, и детектор отмечает это сразу.
Считают зелёный прогон с детектором доказательством отсутствия гонок, а воспроизведение строят на time.Sleep вместо sync.WaitGroup. Детектор ничего не доказывает, а сон только маскирует проблему: такой тест проходит у автора и падает через раз на другой машине.