В чем заключается проблема блокировки нескольких мьютексов в Go и как ее избежать?

Ответ

Основная проблема при блокировке нескольких мьютексов — это взаимная блокировка (deadlock). Она возникает, когда две или более горутины бесконечно ждут друг друга, чтобы освободить ресурс.

Пример Deadlock

Представьте две горутины:

  • Горутина 1: блокирует mu1, затем пытается заблокировать mu2.
  • Горутина 2: блокирует mu2, затем пытается заблокировать mu1.

Если Горутина 1 успела заблокировать mu1, а Горутина 2 — mu2, то ни одна из них не сможет продолжить выполнение. Горутина 1 будет ждать mu2 (занят Горутиной 2), а Горутина 2 будет ждать mu1 (занят Горутиной 1).

// НЕПРАВИЛЬНЫЙ ПРИМЕР, ВЕДУЩИЙ К DEADLOCK

// Горутина 1
go func() {
    mu1.Lock()
    time.Sleep(100 * time.Millisecond) // Даем время другой горутине захватить второй мьютекс
    mu2.Lock()
    // ... работа
    mu2.Unlock()
    mu1.Unlock()
}()

// Горутина 2
go func() {
    mu2.Lock()
    mu1.Lock()
    // ... работа
    mu1.Unlock()
    mu2.Unlock()
}()

Решение: Канонический порядок блокировки

Чтобы избежать deadlock, необходимо всегда захватывать мьютексы в одном и том же строго определенном порядке во всех горутинах. Разблокировку следует производить в обратном порядке.

// ПРАВИЛЬНЫЙ ПРИМЕР

// Везде сперва блокируем mu1, затем mu2
func process() {
    mu1.Lock()
    mu2.Lock()

    // ... критическая секция

    mu2.Unlock()
    mu1.Unlock()
}

Лучшая практика: Использование defer

Использование defer гарантирует, что мьютекс будет освобожден, даже если функция завершится с паникой или будет иметь несколько точек выхода (return).

func processSafely() {
    mu1.Lock()
    defer mu1.Unlock() // Сработает в самом конце функции

    mu2.Lock()
    defer mu2.Unlock() // Сработает перед mu1.Unlock()

    // ... работа с данными
}