Ответ
Основная проблема при блокировке нескольких мьютексов — это взаимная блокировка (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()
// ... работа с данными
}