Что такое mutex, какие они бывают и как их использовать?go-48

Мьютекс (mutex, mutual exclusion) — это примитив синхронизации, который обеспечивает эксклюзивный доступ к общим ресурсам в многопоточной среде.

Основные типы мьютексов в Go

1. sync.Mutex - стандартный мьютекс

Базовый мьютекс с двумя методами:

  • Lock() - захватывает мьютекс (блокирует, если уже захвачен)
  • Unlock() - освобождает мьютекс
var mu sync.Mutex
var sharedData int

func increment() {
    mu.Lock()
    defer mu.Unlock() // Важно использовать defer для гарантированного освобождения
    sharedData++
}

2. sync.RWMutex - читатель-писатель мьютекс

Оптимизирован для сценариев с частым чтением и редкой записью:

  • Lock()/Unlock() - для записи (эксклюзивный доступ)
  • RLock()/RUnlock() - для чтения (множественный доступ)
var rwMu sync.RWMutex
var cache map[string]string

func get(key string) string {
    rwMu.RLock()
    defer rwMu.RUnlock()
    return cache[key]
}

func set(key, value string) {
    rwMu.Lock()
    defer rwMu.Unlock()
    cache[key] = value
}

Правила использования мьютексов

  1. Всегда освобождайте мьютекс (лучше через defer)
  2. Не держите мьютекс слишком долго - только для критической секции
  3. Избегайте вложенных блокировок (может привести к deadlock)
  4. Используйте RWMutex для read-heavy workload

Распространенные ошибки

1. Забытый Unlock

// Плохо:
mu.Lock()
// что-то пошло не так...
return // мьютекс остался заблокированным
mu.Unlock()

// Хорошо:
mu.Lock()
defer mu.Unlock()

2. Deadlock

var mu sync.Mutex

func a() {
    mu.Lock()
    defer mu.Unlock()
    b() // вызов функции, которая тоже пытается захватить мьютекс
}

func b() {
    mu.Lock()
    defer mu.Unlock()
    // ...
}

3. Гонка при "шаблонном" использовании

// Небезопасно:
if len(slice) > 0 {
    mu.Lock()
    v := slice[0] // Гонка данных - slice мог измениться
    mu.Unlock()
}

// Безопасно:
mu.Lock()
defer mu.Unlock()
if len(slice) > 0 {
    v := slice[0]
}

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

1. TryLock

if mu.TryLock() {
    defer mu.Unlock()
    // Критическая секция
} else {
    // Альтернативная логика
}

2. Встраивание мьютекса в структуру

type SafeCounter struct {
    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.Lock()
    defer c.Unlock()
    c.count++
}

3. Группировка защищаемых данных с мьютексом

type ProtectedData struct {
    mu    sync.RWMutex
    items map[int]string
}

func (p *ProtectedData) Get(id int) string {
    p.mu.RLock()
    defer p.mu.RUnlock()
    return p.items[id]
}

Производительность мьютексов

  1. sync.Mutex быстрее при частых записях
  2. sync.RWMutex эффективнее при частом чтении
  3. atomic операции быстрее для простых счетчиков
// Сравнение производительности
BenchmarkMutex-8       100000000    10.2 ns/op
BenchmarkRWMutex-8     200000000    7.5 ns/op (для чтения)
BenchmarkAtomic-8      500000000    3.1 ns/op

Резюмируем

  1. sync.Mutex - базовый мьютекс для эксклюзивного доступа
  2. sync.RWMutex - оптимизирован для read-heavy сценариев
  3. Правила использования:
    • Всегда освобождать (лучше через defer)
    • Минимизировать время удержания
    • Избегать вложенных блокировок
  4. Оптимизации:
    • TryLock для неблокирующих операций
    • Группировка данных с мьютексом
    • Выбор подходящего типа мьютекса

Мьютексы — фундаментальный инструмент для безопасной работы с общими данными в Go, но требуют аккуратного использования.