← Назад к вопросам

Для чего нужен RWMutex?

1.2 Junior🔥 241 комментариев
#Конкурентность и горутины#Основы Go

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

RWMutex: Read-Write Mutex в Go

Определение

RWMutex (Read-Write Mutex) — это примитив синхронизации, который разрешает одновременное чтение из ресурса множеством горутин, но исключительный доступ при записи.

Это оптимизация обычного Mutex для сценариев, где читают гораздо чаще, чем пишут.

Сравнение: Mutex vs RWMutex

// Обычный Mutex
var mu sync.Mutex
var data string

func ReadData() string {
    mu.Lock()      // ЖДЁТ если кто-то пишет
    defer mu.Unlock()
    return data    // Блокирует всех остальных читателей!
}

func WriteData(s string) {
    mu.Lock()      // ЖДЁТ если кто-то читает
    defer mu.Unlock()
    data = s       // Исключительный доступ
}
// RWMutex
var rw sync.RWMutex
var data string

func ReadData() string {
    rw.RLock()     // Много читателей могут быть одновременно!
    defer rw.RUnlock()
    return data
}

func WriteData(s string) {
    rw.Lock()      // Писатель блокирует всех читателей
    defer rw.Unlock()
    data = s
}

Основной сценарий использования

┌─ ReadData()   ─ RLock ─ [читаю] ─ RUnlock ─┐
│                                             │ Все могут быть одновременно!
├─ ReadData()   ─ RLock ─ [читаю] ─ RUnlock ─┤
│                                             │
└─ ReadData()   ─ RLock ─ [читаю] ─ RUnlock ─┘

┌─ WriteData()  ─ Lock ─ [пишу] ─ Unlock ───┐
│ Все другие блокированы (читатели и писатели)

Интерфейс RWMutex

type RWMutex struct {
    // unexported fields
}

// Методы для читателей
func (rw *RWMutex) RLock()      // Захватить блокировку для чтения
func (rw *RWMutex) RUnlock()    // Отпустить блокировку для чтения
func (rw *RWMutex) RLocker() sync.Locker  // Получить Locker для чтения

// Методы для писателей
func (rw *RWMutex) Lock()       // Захватить блокировку для записи
func (rw *RWMutex) Unlock()     // Отпустить блокировку для записи

Правила использования RWMutex

Правило 1: Читатели не блокируют друг друга

var rw sync.RWMutex
var cache map[int]string = make(map[int]string)

func GetFromCache(id int) string {
    rw.RLock()      // Неблокирующее чтение
    defer rw.RUnlock()
    return cache[id]
}

func main() {
    // Много горутин могут читать одновременно
    for i := 0; i < 1000; i++ {
        go GetFromCache(i)
    }
    time.Sleep(time.Second)
}

Правило 2: Писатель блокирует всех

func SetInCache(id int, value string) {
    rw.Lock()       // Исключительный доступ
    defer rw.Unlock()
    cache[id] = value  // Все читатели и писатели ждут!
}

Правило 3: Очерёдность справедлива

// Если писатель ждёт, новые читатели должны дождаться его
var rw sync.RWMutex
var counter int

// Читатели (высокочастотные)
for i := 0; i < 10; i++ {
    go func() {
        for {
            rw.RLock()
            _ = counter  // Чтение
            rw.RUnlock()
        }
    }()
}

// Писатель (редкий)
go func() {
    time.Sleep(time.Second)
    rw.Lock()
    counter++    // Запись — не голодает
    rw.Unlock()
}()

Практические примеры

Пример 1: Кеш с одновременным чтением

type Cache struct {
    rw    sync.RWMutex
    data  map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.rw.RLock()           // Множество горутин могут читать
    defer c.rw.RUnlock()
    value, ok := c.data[key]
    return value, ok
}

func (c *Cache) Set(key, value string) {
    c.rw.Lock()            // Исключительный доступ
    defer c.rw.Unlock()
    c.data[key] = value
}

func main() {
    cache := &Cache{data: make(map[string]string)}
    
    // Множество читателей
    for i := 0; i < 100; i++ {
        go func() {
            for {
                _, _ = cache.Get("key1")
            }
        }()
    }
    
    // Редкие писатели
    for i := 0; i < 2; i++ {
        go func(id int) {
            for {
                cache.Set("key1", fmt.Sprintf("value%d", id))
                time.Sleep(time.Second)
            }
        }(i)
    }
    
    time.Sleep(10 * time.Second)
}

Пример 2: Конфигурация (читается часто, меняется редко)

type Config struct {
    rw       sync.RWMutex
    settings map[string]interface{}
}

func (c *Config) GetSetting(key string) interface{} {
    c.rw.RLock()            // Быстрое чтение
    defer c.rw.RUnlock()
    return c.settings[key]
}

func (c *Config) UpdateSettings(settings map[string]interface{}) {
    c.rw.Lock()             // Редкая запись
    defer c.rw.Unlock()
    for k, v := range settings {
        c.settings[k] = v
    }
}

func main() {
    config := &Config{settings: make(map[string]interface{})}
    config.settings["timeout"] = 30
    config.settings["retries"] = 3
    
    // Много горутин читают конфиг
    for i := 0; i < 1000; i++ {
        go func() {
            for {
                timeout := config.GetSetting("timeout")
                _ = timeout
            }
        }()
    }
    
    // Администратор изредка обновляет
    go func() {
        for {
            time.Sleep(10 * time.Second)
            config.UpdateSettings(map[string]interface{}{
                "timeout": 60,
            })
        }
    }()
    
    time.Sleep(1 * time.Minute)
}

Пример 3: User Repository

type UserRepository struct {
    rw    sync.RWMutex
    users map[int]*User
}

func (repo *UserRepository) GetUser(id int) (*User, error) {
    repo.rw.RLock()      // Множество читателей
    defer repo.rw.RUnlock()
    
    user, ok := repo.users[id]
    if !ok {
        return nil, fmt.Errorf("user not found")
    }
    return user, nil
}

func (repo *UserRepository) UpdateUser(id int, user *User) error {
    repo.rw.Lock()       // Один писатель
    defer repo.rw.Unlock()
    
    if _, ok := repo.users[id]; !ok {
        return fmt.Errorf("user not found")
    }
    repo.users[id] = user
    return nil
}

func (repo *UserRepository) ListUsers() []*User {
    repo.rw.RLock()      // Чтение
    defer repo.rw.RUnlock()
    
    users := make([]*User, 0, len(repo.users))
    for _, u := range repo.users {
        users = append(users, u)
    }
    return users
}

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

✅ Используй RWMutex когда:

  • Чтение гораздо чаще, чем запись (80% reads vs 20% writes)
  • Критичны задержки при чтении
  • Есть много горутин-читателей
  • Защищаемые данные большие

❌ Используй обычный Mutex когда:

  • Чтение и запись примерно одинаковые
  • Очень мало конкуренции
  • Критичны задержки при записи
  • Простота важнее оптимизации

Преимущества и недостатки

АспектRWMutexMutex
ЧтениеБыстро (без блокировки)Медленнее (блокирует)
ЗаписьНормальноНормально
СложностьВышеНиже
OverheadБольшеМеньше
СправедливостьХорошаяХорошая

Антипаттерны

// ❌ Неправильно: забыли RUnlock
func ReadData() {
    rw.RLock()
    data := getData()
    // Забыли rw.RUnlock() → дедлок!
    return data
}

// ✅ Правильно: используй defer
func ReadData() {
    rw.RLock()
    defer rw.RUnlock()
    return getData()
}

// ❌ Неправильно: модификация данных в RLock
func BuggyRead() {
    rw.RLock()
    defer rw.RUnlock()
    data["key"] = "value"  // ❌ Race condition!
}

// ✅ Правильно: только чтение в RLock
func SafeRead() {
    rw.RLock()
    defer rw.RUnlock()
    return data["key"]
}

Заключение

RWMutex нужен для:

  • Оптимизации систем с частым чтением (кеши, конфиги)
  • Масштабирования параллельных операций чтения
  • Снижения contention при высоконагруженных сценариях

Это критичный инструмент для высокопроизводительного параллельного кода в Go.

Для чего нужен RWMutex? | PrepBro