← Назад к вопросам
Для чего нужен 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 когда:
- Чтение и запись примерно одинаковые
- Очень мало конкуренции
- Критичны задержки при записи
- Простота важнее оптимизации
Преимущества и недостатки
| Аспект | RWMutex | Mutex |
|---|---|---|
| Чтение | Быстро (без блокировки) | Медленнее (блокирует) |
| Запись | Нормально | Нормально |
| Сложность | Выше | Ниже |
| 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.