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

Что произойдет, если за RWMutex последует обычный Mutex?

3.0 Senior🔥 141 комментариев
#Конкурентность и горутины

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

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

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

Взаимодействие RWMutex и обычного Mutex в Go

Если за RWMutex в программе на Go следует обычный Mutex, это может привести к неочевидным проблемам с производительностью, взаимоблокировкам (deadlock) или состоянию гонки (race condition), в зависимости от контекста их использования. Ключевая проблема заключается в том, что эти два типа мьютексов не координируются между собой — они являются независимыми примитивами синхронизации.

Основные риски и сценарии

1. Нарушение логической последовательности блокировок

Если код пытается захватить RWMutex для чтения, а затем Mutex для записи (или наоборот) без четкого протокола, это может вызвать взаимоблокировку. Пример:

package main

import (
    "sync"
    "time"
)

var (
    rwMu sync.RWMutex
    mu   sync.Mutex
    data int
)

func reader() {
    rwMu.RLock() // Захватываем RWMutex для чтения
    defer rwMu.RUnlock()
    
    time.Sleep(10 * time.Millisecond) // Имитация работы
    
    mu.Lock() // Пытаемся захватить обычный Mutex
    defer mu.Unlock()
    
    // Работа с данными...
}

func writer() {
    mu.Lock() // Захватываем обычный Mutex
    defer mu.Unlock()
    
    time.Sleep(10 * time.Millisecond)
    
    rwMu.Lock() // Пытаемся захватить RWMutex для записи
    defer rwMu.Unlock()
    
    data = 42
}

func main() {
    go reader()
    go writer()
    time.Sleep(1 * time.Second)
}

В этом примере может возникнуть deadlock:

  • reader захватывает RWMutex для чтения, затем пытается захватить mu
  • writer захватывает mu, затем пытается захватить RWMutex для записи
  • writer ждет, пока все читатели освободят RWMutex, но reader ждет, пока writer освободит mu

2. Потеря преимуществ RWMutex

Основное преимущество RWMutex — возможность параллельного чтения несколькими горутинами. Если после RWMutex всегда следует Mutex, это преимущество теряется:

func process() {
    rwMu.RLock() // Множество читателей могут войти одновременно
    defer rwMu.RUnlock()
    
    mu.Lock() // Но здесь они сериализуются!
    defer mu.Unlock()
    
    // Критическая секция
}

В этом случае все читатели будут блокироваться на mu.Lock(), сводя на нет преимущества параллельного чтения.

3. Сложность отладки и поддержки

Такая комбинация усложняет понимание кода и делает его склонным к скрытым deadlock:

func operation1() {
    rwMu.RLock()
    // ... работа ...
    mu.Lock() // Точка потенциальной блокировки
    // ...
}

func operation2() {
    mu.Lock()
    // ... работа ...
    rwMu.Lock() // Другая точка потенциальной блокировки
    // ...
}

Правильные подходы к решению

Вариант 1: Использовать только RWMutex

Если нужны и чтение, и запись, но с разными режимами доступа:

var rwMu sync.RWMutex
var resource map[string]int

func readValue(key string) int {
    rwMu.RLock()
    defer rwMu.RUnlock()
    return resource[key]
}

func writeValue(key string, value int) {
    rwMu.Lock()
    defer rwMu.Unlock()
    resource[key] = value
}

Вариант 2: Четкий протокол блокировок

Если необходимы оба типа мьютексов, установите строгий порядок захвата:

func safeOperation() {
    // Всегда захватываем в одном порядке: сначала mu, потом rwMu
    mu.Lock()
    defer mu.Unlock()
    
    rwMu.Lock()
    defer rwMu.Unlock()
    
    // Работа с данными
}

Вариант 3: Рефакторинг архитектуры

Пересмотрите структуру данных — возможно, нужна более простая модель синхронизации:

// Вместо двух мьютексов используем каналы
type SafeData struct {
    requests chan func(map[string]int)
    data     map[string]int
}

func NewSafeData() *SafeData {
    sd := &SafeData{
        requests: make(chan func(map[string]int)),
        data:     make(map[string]int),
    }
    go sd.run()
    return sd
}

func (sd *SafeData) run() {
    for op := range sd.requests {
        op(sd.data)
    }
}

Рекомендации по избежанию проблем

  1. Избегайте смешивания разных типов мьютексов без крайней необходимости
  2. Используйте инструменты обнаружения:
    go run -race main.go
    
  3. Применяйте паттерн "всегда один порядок" для всех блокировок
  4. Документируйте протоколы блокировок в сложных системах
  5. Рассмотрите альтернативы — каналы, sync.Map, атомарные операции

Заключение

Использование RWMutex с последующим Mutex допустимо только при тщательном проектировании и понимании порядка захвата блокировок. В большинстве случаев такая комбинация указывает на проблемы в архитектуре синхронизации. Прежде чем применять эту схему, убедитесь, что:

  • Существует четкий, документированный протокол порядка блокировок
  • Вы полностью контролируете все пути выполнения кода
  • У вас есть тесты на race condition и deadlock
  • Вы рассмотрели более простые альтернативы

Правильное использование примитивов синхронизации — ключ к созданию надежных и производительных concurrent-программ на Go.

Что произойдет, если за RWMutex последует обычный Mutex? | PrepBro