Как использовать мьютексы для защиты мапы при параллельном доступе в Go
Что проверяют
Понимаете ли вы, что обычная map в Go не потокобезопасна и что происходит при гонке. Следом почти всегда спрашивают про sync.RWMutex и sync.Map: когда что выбирать.
Как отвечать
Если одна горутина пишет в map, а другая в это время пишет или читает, рантайм может обнаружить это и завершить процесс с fatal error: concurrent map writes. Это не паника, recover её не перехватит. Гонки, которые рантайм пропустил, ловит детектор: go test -race.
Стандартное решение: структура, где мьютекс лежит рядом с мапой, а доступ идёт только через методы.
sync.Mutex: в каждом методеmu.Lock()и сразуdefer mu.Unlock(). Нулевое значение мьютекса готово к работе, инициализировать его не нужно;sync.RWMutex: читатели берутRLock()и работают параллельно, писатель берётLock()эксклюзивно. Выигрыш есть при заметном перевесе чтения, при частой записи он бывает медленнее обычного мьютекса;- дженерики:
type SafeMap[K comparable, V any] structизбавляет от копий под каждый тип; - составные операции: логику «прочитать, решить, записать» делайте одним методом под одной блокировкой (
GetOrSet,Update(fn)), а не двумя вызовами подряд.
Критическую секцию держат короткой, без сетевых вызовов под замком. Структуру с мьютексом передают по указателю: копия получает свой замок, и go vet на это ругается.
sync.Map по документации оптимизирован для двух случаев: ключ пишется один раз и много читается, либо горутины работают с непересекающимися наборами ключей. В остальных случаях мапа с мьютексом проще и типобезопаснее. При высокой конкуренции помогает шардирование: несколько мап со своими мьютексами, ключ распределяется по хэшу.
Где спотыкаются
Защищают только запись, а чтение оставляют без замка. Гонка остаётся, и процесс всё равно может упасть с concurrent map read and map write.
Возвращают наружу саму мапу или итерируют по ней после Unlock. На этом защита заканчивается.
Из метода, который уже держит Lock(), вызывают другой метод той же структуры, который тоже берёт Lock(). Мьютексы в Go нереентерабельные, это дедлок.