Как оптимизировать блокировки при работе с мапой и слайсом в Go, чтобы не блокировать весь слайс
Что проверяют
Знаете ли вы способы сузить область блокировки: шардирование, раздельные элементы, атомики, копирование при записи. Следом обычно спрашивают, когда sync.Map действительно быстрее мапы с мьютексом и почему append всё портит.
Как отвечать
Начните с того, что обычная мапа в Go не потокобезопасна: одновременная запись роняет процесс с fatal error: concurrent map writes, и recover это не перехватит. Дальше способы не держать один замок на всё:
- шардирование: вместо одной мапы N штук, у каждой свой
sync.RWMutex, шард выбирается по хэшу ключа черезhash/maphash; - свой индекс у каждой горутины: разные элементы слайса это разные ячейки памяти. Если слайс создан заранее через
make([]T, n)и каждая горутина пишет только вresults[i], мьютекс не нужен вовсе, хватитsync.WaitGroup; - блокировка на элемент: слайс структур, в каждой свой
sync.Mutex, или[]atomic.Int64для счётчиков. Работает, пока длина слайса не меняется; - копирование при записи: данные, которые часто читают и редко меняют, хранят в
atomic.Pointer[T]. Читатель берёт снимок без блокировок, писатель собирает новую копию и подменяет указатель; - короткая критическая секция: вычисления и ввод-вывод делаются до
Lock, под замком только само чтение или запись.
У sync.Map в документации названы два сценария: ключ пишется один раз и много раз читается, или горутины работают с непересекающимися наборами ключей. На смешанной нагрузке мапа с мьютексом часто не хуже.
Решение подтверждают замером: бенчмарк с b.RunParallel и профиль блокировок в pprof после runtime.SetMutexProfileFraction.
Где спотыкаются
Делают блокировку на элемент и одновременно append в тот же слайс. При росте ёмкости элементы копируются в новый массив, и горутины с указателями на старые блокируют уже устаревшие копии.
Берут RWMutex везде, «потому что быстрее». При частой записи и коротких секциях он проигрывает обычному Mutex, а повторный RLock в той же горутине при ожидающем писателе даёт дедлок.
Советуют sync.Map как универсальную замену мапе с мьютексом, не зная, под какие сценарии она сделана.