Почему при конкурентной записи в map в Go возникают ошибки и как правильно работать с map в многопоточном режиме
Что проверяют
Знаете ли вы, что конкурентная запись в map это не «иногда теряются данные», а немедленная смерть процесса, и умеете ли выбирать защиту под профиль нагрузки, а не по привычке.
Как отвечать
Встроенная map не потокобезопасна, и рантайм следит за этим специально: у таблицы есть флаг записи, и если в неё лезут во время записи, рантайм бросает fatal error: concurrent map writes. Важная деталь, которую почти никто не проговаривает: это не паника, recover её не перехватывает, падает весь процесс. Проверка дешёвая и потому неполная, часть гонок она пропускает, и тогда вместо падения получится порча данных или зависание. Ищут такое через go test -race.
Причина в устройстве: при росте map перераспределяет бакеты, и параллельный доступ идёт по данным, которые уже переезжают. Одновременное чтение без записи безопасно, но ровно до первой записи.
Варианты защиты:
sync.RWMutexвокруг обычной map: понятный вариант по умолчанию, читатели не мешают друг другу;- шардирование на несколько map со своими замками, ключ раскладывается по хешу. Снимает давку на одном горячем замке;
sync.Mapиз стандартной библиотеки: он заточен под два сценария, запись один раз и много чтений либо непересекающиеся наборы ключей у разных горутин. На смешанной нагрузке проигрывает мьютексу и лишает типизации;- владение: map держит одна горутина, остальные общаются с ней каналом.
type Store struct {
mu sync.RWMutex
m map[string]int
}
func (s *Store) Set(k string, v int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[k] = v
}Где спотыкаются
Называют это паникой и обещают обернуть в recover. Ответ сразу показывает, что человек такого падения в бою не видел.
Записывают sync.Map в сторонние пакеты или предлагают как универсальную замену обычной map. Это стандартная библиотека и узкий инструмент, а не быстрая версия map.
Закрывают замком запись и оставляют чтение голым, либо отдают наружу саму map из-под замка. Читателю тоже нужен RLock, а возвращать надо копию, иначе вызывающий спокойно читает структуру, пока её меняют.