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

Какие типы mutex используешь?

2.3 Middle🔥 131 комментариев
#Конкурентность и горутины

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

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

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

Типы мьютексов в Go: от базовых до продвинутых

В Go существует несколько типов мьютексов, каждый из которых решает специфические задачи синхронизации. Вот основные из них, которые я регулярно использую в production-коде:

1. Стандартный sync.Mutex

Базовый взаимный исключитель (mutual exclusion), который предоставляет эксклюзивный доступ к общему ресурсу.

package main

import (
    "fmt"
    "sync"
    "time"
)

type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.v[key]++
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    var wg sync.WaitGroup
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Inc("somekey")
        }()
    }
    wg.Wait()
    fmt.Println(c.v["somekey"]) // Всегда 1000
}

Ключевые особенности:

  • Только одна горутина может захватить мьютекс в любой момент времени
  • Использование defer для гарантированного разблокирования
  • При попытке повторного захвата той же горутиной приводит к deadlock

2. sync.RWMutex (Read-Write Mutex)

Оптимизированный мьютекс для сценариев "много читателей, мало писателей". Позволяет множественным горутинам читать данные одновременно, но эксклюзивно блокирует при записи.

type ConfigManager struct {
    mu   sync.RWMutex
    config map[string]string
}

func (cm *ConfigManager) Get(key string) string {
    cm.mu.RLock()         // Блокировка для чтения (множественная)
    defer cm.mu.RUnlock()
    return cm.config[key]
}

func (cm *ConfigManager) Set(key, value string) {
    cm.mu.Lock()          // Эксклюзивная блокировка для записи
    defer cm.mu.Unlock()
    cm.config[key] = value
}

Преимущества RWMutex:

  • Высокая производительность в read-heavy сценариях
  • Минимальные блокировки при параллельном чтении
  • Детерминированная синхронизация при обновлениях

3. Каналы как альтернатива мьютексам

Идиоматичный Go-подход "Don't communicate by sharing memory; share memory by communicating":

// Вместо мьютекса используем канал-мультиплексор
type Counter struct {
    requests chan struct{}
    counts   chan int
}

func NewCounter() *Counter {
    c := &Counter{
        requests: make(chan struct{}),
        counts:   make(chan int),
    }
    go c.run()
    return c
}

func (c *Counter) run() {
    var count int
    for {
        select {
        case <-c.requests:
            count++
            c.counts <- count
        }
    }
}

4. sync.Map для специфических случаев

Специализированная concurrent-мапа, использующая внутренние мьютексы оптимальным образом:

var m sync.Map

// Параллельные операции безопасны
m.Store("key", "value")
value, ok := m.Load("key")
m.Delete("key")

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

  • Когда ключи почти не обновляются, но часто читаются
  • При работе с несколькими независимыми наборами ключей
  • В кэширующих системах с редкими обновлениями

5. Атомарные операции sync/atomic

Для простых счетчиков и флагов атомарные операции эффективнее мьютексов:

import "sync/atomic"

type AtomicCounter struct {
    value int64
}

func (c *AtomicCounter) Add(delta int64) {
    atomic.AddInt64(&c.value, delta)
}

func (c *AtomicCounter) Value() int64 {
    return atomic.LoadInt64(&c.value)
}

Критерии выбора типа синхронизации

  1. sync.Mutex выбираю когда:

    • Нужна простая эксклюзивная блокировка
    • Операции записи частые
    • Логика критической секции сложная
  2. sync.RWMutex предпочитаю для:

    • Конфигураций, кэшей, справочников
    • Read-heavy workload (90%+ операций чтения)
    • Данных, которые редко обновляются
  3. Каналы использую когда:

    • Нужна координация между горутинами
    • Требуются таймауты или отмена операций
    • Реализуется паттерн worker pool
  4. Атомарные операции применяю для:

    • Счетчиков, флагов состояния
    • Простых числовых операций
    • Когда производительность критична

Продвинутые практики

Graceful shutdown с мьютексами:

type Service struct {
    mu      sync.RWMutex
    stopped bool
}

func (s *Service) Stop() {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.stopped = true
}

func (s *Service) Process() error {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    if s.stopped {
        return errors.New("service stopped")
    }
    // ... обработка
}

Важные нюансы:

  • Всегда проверяю не происходит ли захват мьютекса в разном порядке (риск deadlock)
  • Использую defer для Unlock везде, кроме оптимизированных hot-paths
  • Для отладки deadlock применяю pprof и MutexProfile
  • В тестах использую -race флаг для детекции гонок данных

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

Выбор типа мьютекса — это всегда компромисс между простотой, производительностью и безопасностью. Я начинаю с простого sync.Mutex, перехожу на RWMutex при доказанной необходимости, и рассматриваю каналы или атомарные операции для специфических паттернов доступа.