Для чего нужен sync.RWMutex и когда его использовать в Go
Что проверяют
Знаете ли вы, как ведёт себя sync.RWMutex и когда он действительно выгоднее обычного sync.Mutex, а когда только мешает. Следом часто спрашивают, можно ли повысить блокировку чтения до записи и почему рекурсивный RLock опасен.
Как отвечать
Суть и поведение:
- семантика:
RLock/RUnlockпускают любое число читателей одновременно,Lock/Unlockдают эксклюзивный доступ писателю. Нулевое значение готово к работе, копировать мьютекс после первого использования нельзя, это ловитgo vet; - приоритет писателя: как только горутина ждёт в
Lock, новыеRLockблокируются до конца записи. Так писатели не голодают; - нет повышения: из-под
RLockнельзя взятьLock, будет взаимоблокировка. Нужно отпустить чтение, взять запись и заново проверить условие, потому что данные могли измениться; - рекурсивное чтение запрещено: повторный
RLockв той же горутине при ожидающем писателе вешает программу; - цена:
RWMutexтяжелееMutex, а на многоядерной машине все читатели бьются в один атомарный счётчик, и кэш-линия прыгает между ядрами.
Когда брать: чтений заметно больше, чем записей, и секция под блокировкой не пустяковая, например поиск по большой структуре. Для чтения одного значения из map обычный Mutex часто не хуже, решение принимают по бенчмарку с флагом -cpu и профилю блокировок, а не по интуиции.
Назовите альтернативы: atomic.Pointer[T] для конфигурации, которую целиком подменяют копией; sync.Map для кэшей, где ключ пишется один раз и много раз читается; шардирование map на части со своим мьютексом у каждой.
Где спотыкаются
Ставят RWMutex везде «на всякий случай» и на коротких секциях получают результат хуже, чем с Mutex.
Пишут под RLock. Гонку это не устраняет, go test -race её покажет, а конкурентная запись в map и вовсе роняет процесс с concurrent map writes.
Забывают RUnlock на раннем выходе из функции без defer, и первый же писатель висит навсегда.