Как обеспечить конкурентный доступ к мапе в Go?
Что проверяют
Знаете ли вы, что рантайм сам роняет программу при конкурентной записи, и умеете ли выбирать между мьютексом и sync.Map.
Как отвечать
Встроенная карта не потокобезопасна, и это не просто гонка: рантайм специально обнаруживает одновременную запись и завершает программу с сообщением concurrent map writes. То есть тихой порчи данных не будет, будет падение.
Способы, по частоте применения:
1. Мьютекс, основной вариант. Обычно sync.RWMutex, если чтений заметно больше записей:
type Cache struct {
mu sync.RWMutex
m map[string]int
}
func (c *Cache) Get(k string) (int, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.m[k]
return v, ok
}
func (c *Cache) Set(k string, v int) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[k] = v
}Карту прячут внутрь структуры, чтобы снаружи не было доступа в обход блокировки.
2. sync.Map. Вопреки названию, это не универсальная замена. Документация прямо называет два сценария, где она выигрывает: ключ пишется один раз и потом много читается, либо разные горутины работают с непересекающимися наборами ключей. В остальных случаях она медленнее мьютекса, а ещё не типизирована и требует приведения типов.
3. Шардирование. При очень высокой конкуренции карту делят на несколько частей со своими мьютексами, шард выбирается хешем ключа. Это снимает борьбу за одну блокировку.
4. Владелец. Картой владеет одна горутина, остальные шлют ей запросы через канал. Дороже по накладным расходам, зато синхронизация не нужна вовсе.
Проверять всё это надо детектором гонок: go test -race и go run -race.
Где спотыкаются
Берут sync.Map по умолчанию, считая её просто безопасной картой.
Защищают запись, но читают без блокировки.
Держат блокировку во время долгой операции внутри критической секции.
Не запускают тесты с детектором гонок.