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

Как залочить два Mutex?

2.0 Middle🔥 161 комментариев
#Конкурентность и горутины#Производительность и оптимизация

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

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

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

Как залочить два мьютекса в Go

В Go для координированного захвата нескольких мьютексов существует несколько подходов, каждый со своими преимуществами и подводными камнями. Ниже рассмотрю основные методы с акцентом на предотвращение взаимной блокировки (deadlock).

1. Последовательный захват в строгом порядке (самый надёжный способ)

Самый простой и рекомендуемый подход — всегда захватывать мьютексы в одинаковом порядке во всех горутинах. Это гарантирует избежание взаимных блокировок.

package main

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

func main() {
    var mu1, mu2 sync.Mutex
    
    // Все горутины захватывают mu1, затем mu2
    go func() {
        mu1.Lock()
        defer mu1.Unlock()
        
        time.Sleep(10 * time.Millisecond) // Имитация работы
        
        mu2.Lock()
        defer mu2.Unlock()
        
        fmt.Println("Горутина 1: захватила оба мьютекса")
    }()
    
    go func() {
        mu1.Lock() // Всегда сначала mu1
        defer mu1.Unlock()
        
        time.Sleep(5 * time.Millisecond)
        
        mu2.Lock() // Затем mu2
        defer mu2.Unlock()
        
        fmt.Println("Горутина 2: захватила оба мьютекса")
    }()
    
    time.Sleep(100 * time.Millisecond)
}

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

  • Простота реализации
  • Гарантированное отсутствие deadlock
  • Минимальные накладные расходы

Недостатки:

  • Требует дисциплины от разработчиков
  • Может привести к избыточной блокировке (conservative locking)

2. Использование sync.Locker с функцией lockTwo

Для более сложных сценариев можно создать вспомогательную функцию:

func lockTwo(l1, l2 sync.Locker) {
    for {
        l1.Lock()
        
        // Пытаемся захватить второй мьютекс с таймаутом
        locked := tryLockWithTimeout(l2, time.Millisecond)
        if locked {
            return // Оба захвачены
        }
        
        // Не удалось захватить второй — отпускаем первый
        l1.Unlock()
        
        // Даём шанс другим горутинам
        time.Sleep(time.Microsecond)
    }
}

func tryLockWithTimeout(l sync.Locker, timeout time.Duration) bool {
    ch := make(chan bool, 1)
    go func() {
        l.Lock()
        ch <- true
    }()
    
    select {
    case <-ch:
        return true
    case <-time.After(timeout):
        return false
    }
}

Важно: Эта реализация имеет риск голодания (starvation) и менее эффективна, чем метод строгого порядка.

3. Композитный мьютекс (обёртка)

Можно создать структуру, содержащую оба защищаемых ресурса и один общий мьютекс:

type CompositeResource struct {
    mu    sync.Mutex
    data1 DataType1
    data2 DataType2
}

func (cr *CompositeResource) Process() {
    cr.mu.Lock()
    defer cr.mu.Unlock()
    
    // Работаем с data1 и data2 одновременно
}

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

  • Когда два ресурса логически связаны
  • При частом совместном доступе к обоим ресурсам

4. Использование sync.RWMutex для оптимизации

Если операции чтения часты, можно использовать sync.RWMutex:

type DualProtected struct {
    mu1 sync.RWMutex
    mu2 sync.RWMutex
    // поля
}

func (d *DualProtected) ReadBoth() {
    d.mu1.RLock()
    defer d.mu1.RUnlock()
    
    d.mu2.RLock()
    defer d.mu2.RUnlock()
    
    // Чтение данных
}

Критические моменты и лучшие практики

  1. Всегда используйте defer для Unlock (кроме особых случаев оптимизации):

    mu1.Lock()
    defer mu1.Unlock()
    mu2.Lock()
    defer mu2.Unlock()
    
  2. Избегайте вложенных блокировок там, где это возможно. Часто проблему двух мьютексов можно решить рефакторингом:

    • Объединение ресурсов под одним мьютексом
    • Использование каналов для сериализации доступа
    • Применение sync/atomic для простых операций
  3. Тестируйте на deadlock с помощью инструментов:

    go test -race ./...
    
  4. Рассмотрите альтернативы мьютексам:

    • Каналы для коммуникации между горутинами
    • sync.Map для конкурентных мап
    • sync.Once для однократной инициализации

Пример Deadlock (чего избегать)

// НЕПРАВИЛЬНО: взаимная блокировка
go func() {
    mu1.Lock()
    mu2.Lock() // Ждёт, пока mu2 освободится
    // ...
}()

go func() {
    mu2.Lock()
    mu1.Lock() // Ждёт, пока mu1 освободится
    // ...
}()

Резюме: Для захвата двух мьютексов предпочтительным методом является строгий порядок блокировки, документированный и соблюдаемый во всей кодовой базе. Для сложных случаев рассмотрите использование единого композитного мьютекса или рефакторинг архитектуры. Инструменты вроде go test -race и статические анализаторы помогают выявлять потенциальные deadlock на ранних этапах разработки.