Ответ
Основной и самый надежный способ избежать 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, но позволяет его обнаружить и прервать операцию по таймауту, избегая вечного зависания программы.