Когда замена sync.Mutex на sync.RWMutex может ухудшить производительность?
Замена sync.Mutex на sync.RWMutex может ухудшить производительность, если критические секции короткие, конкуренция невысокая или операции записи выполняются часто. RWMutex имеет более сложную внутреннюю координацию: читатели используют атомарные операции, а при ожидании писателя подключаются дополнительные механизмы блокировки. Поэтому параллельное чтение окупает эту стоимость только при действительно высокой конкуренции читателей и редких длительных записях.
Обычный Mutex защищает критическую секцию так, что одновременно внутрь входит только одна горутина. Такой примитив подходит для общего случая и минимизирует накладные расходы.
RWMutex появился как средство для состояния, которое часто читают и редко изменяют. Он позволяет нескольким читателям работать одновременно, но сохраняет взаимоисключение между читателями и писателями.
Предположим, общий кэш читается множеством горутин, а обновляется отдельными горутинами. Кажется логичным сразу выбрать RWMutex, но это решение может оказаться медленнее: чтение может быть настолько коротким, что затраты на управление режимами доступа превысят выигрыш от параллельности.
При наличии ожидающего писателя новые читатели не получают бесконечного приоритета: они могут быть задержаны, чтобы писатель дождался завершения уже начавшихся чтений. Если записи происходят часто, читатели будут регулярно останавливаться, а более сложная синхронизация не даст заметной пользы.
Неверный выбор приводит не к логической ошибке сам по себе, а к лишним задержкам, снижению пропускной способности и менее предсказуемому времени ответа под нагрузкой.
sync.Mutex обычно предпочтительнее, когда критическая секция короткая, содержит смешанные чтения и записи или число одновременно работающих горутин невелико. У него проще семантика: одна блокировка, один владелец критической секции, меньше переходов между состояниями.
sync.RWMutex оправдан, когда одновременно выполняется много длительных операций чтения, записи редки, а защищаемое состояние нельзя безопасно читать без блокировки. Несколько читателей тогда могут использовать ресурс параллельно, пока писатель не запросил эксклюзивный доступ.
Важное ограничение: RWMutex нельзя повышать с режима чтения на режим записи. Горутина, удерживающая RLock, не должна пытаться получить Lock, поскольку ожидающий писатель будет ждать завершения чтения, а сама горутина — писателя. Это может привести к взаимной блокировке.
Нельзя держать блокировку дольше необходимого. Внешние вызовы, сетевые операции и длительные вычисления внутри критической секции обычно нивелируют преимущества любого примитива и увеличивают конкуренцию.
Выбор следует проверять измерениями на реалистичной нагрузке. Нужно сравнивать не только среднее время операции, но и задержки на высоких процентилях, пропускную способность и долю времени ожидания блокировки.
В сервисе есть таблица настроек: сотни горутин читают её при обработке запросов, а обновление выполняется раз в несколько минут. Сначала использовали sync.Mutex, поэтому даже короткие чтения выстраивались в очередь.
Рассматривались три варианта. Замена на RWMutex почти не требовала изменения архитектуры и позволяла параллельные чтения, но добавляла накладные расходы и всё равно блокировала запросы во время обновления. Обычный Mutex был проще, однако сохранял ненужную сериализацию. Третий вариант — публиковать неизменяемый снимок настроек через атомарную замену ссылки — давал чтения без блокировки, но требовал аккуратно создавать новые снимки и не изменять опубликованные данные.
Для небольшого состояния выбрали неизменяемые снимки: обновление собирало новую структуру и атомарно делало её доступной читателям. Это устранило блокировку на пути чтения. Если бы состояние было большим, изменяемым или требовало сложных согласованных операций, более предсказуемым выбором остался бы RWMutex, подтверждённый нагрузочным тестом.
Почему короткая критическая секция часто делает Mutex быстрее RWMutex?
У RWMutex параллельность читателей не бесплатна. Для входа и выхода из режима чтения используются атомарные операции и проверка состояния писателей; при конкуренции могут потребоваться дополнительные операции ожидания. Если защищаемая работа занимает несколько инструкций, Mutex может завершать блокировку быстрее, чем RWMutex организует совместный доступ.
Что происходит с читателями при появлении ожидающего писателя?
Уже вошедшие читатели могут продолжить работу, но новые читатели не должны бесконечно проходить перед ожидающим писателем. Они будут ждать, пока писатель получит возможность захватить блокировку и завершит запись. Поэтому даже при большом числе читателей запись создаёт точку остановки, а частые записи ограничивают выигрыш от параллельного чтения.
Почему нельзя безопасно заменить RLock на Lock внутри одной операции?
Для перехода к записи сначала нужно освободить RLock, а затем получить Lock. Между этими действиями другая горутина может изменить состояние, поэтому ранее прочитанные данные уже нельзя считать актуальными; может потребоваться повторная проверка условия. Если же не освобождать RLock, ожидающий писатель не сможет войти, а текущая горутина не сможет получить Lock, что создаёт взаимную блокировку.