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

Можно ли использовать несколько Mutex в рамках вызова одной функции?

2.0 Middle🔥 172 комментариев
#Конкурентность и горутины#Основы Go

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

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

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

Краткий ответ

Да, можно и часто нужно использовать несколько sync.Mutex в рамках одной функции. Это стандартная практика в Go для защиты разных ресурсов или для реализации сложных сценариев блокировок.

Подробное объяснение

Зачем использовать несколько мьютексов

Использование нескольких мьютексов в одной функции обычно преследует следующие цели:

  1. Защита разных ресурсов - каждый мьютекс защищает свой собственный набор данных
  2. Уменьшение contention (конкуренции) - потоки блокируются только при доступе к конкретному ресурсу
  3. Реализация сложных паттернов - like reader-writer locks, двухфазные блокировки или иерархические блокировки

Пример: защита разных структур данных

package main

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

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

type SafeLogger struct {
    mu      sync.Mutex
    entries []string
}

func processData(counter *SafeCounter, logger *SafeLogger, id int) {
    // Блокируем мьютекс для счетчика
    counter.mu.Lock()
    counter.count++
    currentCount := counter.count
    counter.mu.Unlock()
    
    // Блокируем мьютекс для логгера
    logger.mu.Lock()
    logger.entries = append(logger.entries, 
        fmt.Sprintf("Горутина %d: счетчик = %d", id, currentCount))
    logger.mu.Unlock()
    
    time.Sleep(time.Millisecond * 10)
}

func main() {
    counter := &SafeCounter{count: 0}
    logger := &SafeLogger{entries: make([]string, 0)}
    
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            processData(counter, logger, id)
        }(i)
    }
    
    wg.Wait()
    
    logger.mu.Lock()
    fmt.Printf("Всего записей: %d\n", len(logger.entries))
    for _, entry := range logger.entries {
        fmt.Println(entry)
    }
    logger.mu.Unlock()
}

Важные предостережения и best practices

1. Избегайте deadlock (взаимной блокировки)

При использовании нескольких мьютексов критически важно соблюдать последовательность блокировок, иначе возникает риск deadlock:

// ПРАВИЛЬНО - последовательность блокировок всегда A -> B
func safeOperation(muA, muB *sync.Mutex) {
    muA.Lock()
    defer muA.Unlock()
    
    muB.Lock()
    defer muB.Unlock()
    
    // операции с защищенными ресурсами
}

// ОПАСНО - может привести к deadlock!
func dangerousOperation(muA, muB *sync.Mutex, reverseOrder bool) {
    if reverseOrder {
        muB.Lock()  // другая горутина могла взять muA -> deadlock!
        muA.Lock()
    } else {
        muA.Lock()
        muB.Lock()
    }
    // ...
}

2. Используйте defer для надежного освобождения

func complexOperation(mu1, mu2 *sync.Mutex) error {
    mu1.Lock()
    defer mu1.Unlock()
    
    mu2.Lock()
    defer mu2.Unlock()
    
    // Критическая секция
    if err := doSomething(); err != nil {
        return err  // мьютексы автоматически разблокируются благодаря defer
    }
    
    return nil
}

3. Рассмотрите sync.RWMutex для read-heavy workload

func processWithRWMutex(data *struct {
    mu     sync.RWMutex
    cache  map[string]string
    stats  struct {
        mu   sync.Mutex
        hits int
    }
}, key string) string {
    // Множество горутин могут читать одновременно
    data.mu.RLock()
    value, exists := data.cache[key]
    data.mu.RUnlock()
    
    if exists {
        // Только одна горутина может обновлять stats
        data.stats.mu.Lock()
        data.stats.hits++
        data.stats.mu.Unlock()
    }
    
    return value
}

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

Fine-grained locking (Тонкая блокировка)

type PartitionedData struct {
    partitions []struct {
        mu    sync.Mutex
        data  []int
    }
}

func (pd *PartitionedData) Update(partition, index int, value int) {
    // Блокируем только нужную партицию
    pd.partitions[partition].mu.Lock()
    defer pd.partitions[partition].mu.Unlock()
    
    if index < len(pd.partitions[partition].data) {
        pd.partitions[partition].data[index] = value
    }
}

Two-phase locking (Двухфазная блокировка)

func transferFunds(accountA, accountB *struct {
    mu     sync.Mutex
    balance int
}, amount int) error {
    // Фаза 1: приобретение всех блокировок
    accountA.mu.Lock()
    accountB.mu.Lock()
    
    // Фаза 2: выполнение операций
    if accountA.balance < amount {
        accountA.mu.Unlock()
        accountB.mu.Unlock()
        return fmt.Errorf("недостаточно средств")
    }
    
    accountA.balance -= amount
    accountB.balance += amount
    
    // Освобождение в обратном порядке (не обязательно с defer)
    accountB.mu.Unlock()
    accountA.mu.Unlock()
    
    return nil
}

Когда НЕ стоит использовать несколько мьютексов

  1. Простой сценарий - если защищаете одну логическую сущность
  2. Высокая вероятность deadlock - если не можете гарантировать порядок блокировок
  3. Альтернативы проще - иногда sync.Map, каналы или atomic операции лучше

Вывод

Использование нескольких sync.Mutex в одной функции — это мощный инструмент для:

  • Повышения параллелизма за счет уменьшения contention
  • Защиты независимых ресурсов
  • Реализации сложных параллельных алгоритмов

Ключевые правила безопасности:

  • Соблюдайте строгий порядок блокировок во всей программе
  • Всегда используйте defer для гарантированного освобождения
  • Документируйте инварианты порядка блокировок
  • Тестируйте на race conditions с go test -race

Эта практика широко используется в production-коде Go и является важным навыком для разработки эффективных параллельных систем.