Как избежать взаимной блокировки (deadlock) при работе с несколькими мьютексами?

Ответ

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

Пример:

package main

import (
    "sync"
    "time"
)

var mu1, mu2 sync.Mutex

// Безопасная функция: всегда захватывает mu1, затем mu2
func safeAccess() {
    mu1.Lock()
    defer mu1.Unlock()

    time.Sleep(10 * time.Millisecond) // Имитация работы

    mu2.Lock()
    defer mu2.Unlock()
    // ... работа с общими ресурсами ...
}

// Опасная функция: нарушает порядок захвата, что может привести к deadlock
func unsafeAccess() {
    mu2.Lock() // Захват в обратном порядке
    defer mu2.Unlock()

    time.Sleep(10 * time.Millisecond)

    mu1.Lock()
    defer mu1.Unlock()
    // ... работа с общими ресурсами ...
}

Другие подходы и стратегии:

  • Использование sync.TryLock (доступно в Go 1.18+): Этот метод пытается захватить мьютекс, но не блокирует горутину, если он уже занят. Это позволяет реализовать логику отката: если не удалось захватить второй мьютекс, можно освободить первый и повторить попытку позже.
  • Рефакторинг с использованием каналов: Вместо прямого доступа к общим данным под мьютексами, можно организовать работу через каналы. Например, создать одну горутину-менеджера, которая владеет данными и обрабатывает запросы на их чтение/изменение, получаемые из канала. Это централизует доступ и устраняет потребность в нескольких мьютексах.
  • Ограничение времени ожидания (context): Можно использовать context.WithTimeout для операций, которые могут заблокироваться. Это не предотвращает deadlock, но позволяет его обнаружить и прервать операцию по таймауту, избегая вечного зависания программы.