> Для чего нужен sync.RWMutex и когда его использовать в Go (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Employcity

Стек: Go

> Пример ответа

Короткий ответ

sync.RWMutex - это блокировка с разделением на чтение и запись. Она позволяет множеству горутин одновременно читать данные, но только одной - писать. Используется, когда чтения значительно чаще, чем записи, и данные защищены от гонок. В отличие от sync.Mutex, RWMutex снижает конкуренцию при параллельных чтениях, но добавляет небольшие накладные расходы на управление состоянием. Применяется для кэшей, конфигураций, счётчиков и других структур с преобладанием read-операций.

Подробное объяснение

sync.RWMutex реализует паттерн reader-writer lock. Внутри он содержит два состояния: режим чтения (shared) и режим записи (exclusive). Основные методы:

  • Lock() / Unlock() - эксклюзивная блокировка для записи.
  • RLock() / RUnlock() - разделяемая блокировка для чтения.

Ключевое свойство: если хотя бы одна горутина удерживает RLock, то Lock() будет ждать, пока все читатели не освободят блокировку. При этом новые RLock() могут продолжать захватывать блокировку, что может привести к starvation писателя. В Go 1.18+ это частично решено: RWMutex стал "writer-preferring" - если писатель ждёт, новые читатели не получают блокировку.

Когда использовать:

  • Чтения >> записи (например, 100:1 и выше). Если соотношение примерно равное, sync.Mutex будет эффективнее из-за меньших накладных расходов.
  • Данные не изменяются часто, но к ним обращаются из многих горутин.
  • Нужна атомарность на уровне всей структуры, а не отдельных полей.

Когда не использовать:

  • Если записи частые - RWMutex только добавит сложность и замедлит работу.
  • Если можно обойтись атомарными операциями (atomic.Int64, atomic.Value) - они быстрее.
  • Если структура данных уже потокобезопасна (например, sync.Map для специфичных сценариев).

Важно: RWMutex не защищает от гонок на уровне отдельных полей - только на уровне критической секции. Нельзя копировать мьютекс после использования (это приведёт к панике или некорректной работе). Также нельзя вызывать Unlock() без соответствующего Lock() - это вызовет panic.

На практике

В реальном коде RWMutex часто используется в:

  • Кэшах: чтение из памяти - основная операция, запись - редкая (инвалидация, обновление).
  • Конфигурациях: параметры загружаются при старте, читаются постоянно.
  • Пул соединений: список свободных соединений читается часто, изменяется редко.
  • Счётчики метрик: если нужна согласованность нескольких значений (нельзя через atomic).

Паттерн использования:

GO
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}

Важно: не используйте defer в циклах - это удерживает блокировку дольше, чем нужно. Лучше явно вызывать RUnlock() в конце итерации.

Пример кода

GO
package main
import (
"fmt"
"sync"
)
type Config struct {
mu sync.RWMutex
values map[string]string
}
func NewConfig() *Config {
return &Config{values: make(map[string]string)}
}
func (c *Config) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.values[key]
}
func (c *Config) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.values[key] = value
}
func main() {
cfg := NewConfig()
cfg.Set("timeout", "30s")
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(cfg.Get("timeout"))
}()
}
wg.Wait()
}

Как отвечать на собеседовании

Начните с определения: RWMutex - это блокировка с разделяемым чтением и эксклюзивной записью. Затем объясните, когда применять: преобладание чтений над записями. Приведите пример из практики - кэш или конфигурация. Упомяните trade-off: накладные расходы на управление состоянием против параллелизма чтений. Если спросят про starvation - скажите, что в современных версиях Go реализована writer-preference. Не забудьте упомянуть ограничения: нельзя копировать, нельзя вызывать Unlock без Lock, не защищает отдельные поля.

Что проверяет интервьюер

Интервьюер оценивает:

  • Понимание разницы между Mutex и RWMutex.
  • Умение обосновать выбор на основе соотношения операций.
  • Знание внутреннего устройства: shared/exclusive режимы, проблема starvation.
  • Понимание ограничений и edge cases: копирование мьютекса, panic при неправильном использовании.
  • Способность предложить альтернативы: atomic, sync.Map, шардирование.

Типичные ошибки

  • Использование RWMutex при равном соотношении чтений и записей - только замедляет.
  • Удержание RLock дольше, чем нужно (например, из-за defer в цикле).
  • Копирование структуры с мьютексом после первого использования.
  • Предположение, что RWMutex защищает от гонок на уровне полей - это не так.
  • Игнорирование starvation писателей в старых версиях Go.
  • Использование RWMutex там, где достаточно atomic.Value или atomic.Int64.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью