Ответ
Deadlock (взаимная блокировка) — это ситуация в конкурентном программировании, когда две или более горутины находятся в состоянии бесконечного ожидания, потому что каждая из них ожидает освобождения ресурса, который захвачен другой горутиной из этой же группы.
Классический пример с мьютексами:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu1, mu2 sync.Mutex
var wg sync.WaitGroup
wg.Add(2)
// Горутина 1: захватывает mu1, затем пытается захватить mu2
go func() {
defer wg.Done()
fmt.Println("Горутина 1: захватывает mu1...")
mu1.Lock()
defer mu1.Unlock()
time.Sleep(100 * time.Millisecond)
fmt.Println("Горутина 1: пытается захватить mu2...")
mu2.Lock()
defer mu2.Unlock()
}()
// Горутина 2: захватывает mu2, затем пытается захватить mu1
go func() {
defer wg.Done()
fmt.Println("Горутина 2: захватывает mu2...")
mu2.Lock()
defer mu2.Unlock()
time.Sleep(100 * time.Millisecond)
fmt.Println("Горутина 2: пытается захватить mu1...")
mu1.Lock()
defer mu1.Unlock()
}()
wg.Wait() // Эта строка никогда не будет достигнута
fmt.Println("Программа завершена")
}
В этом примере Горутина 1 захватывает mu1 и ждёт mu2, в то время как Горутина 2 захватывает mu2 и ждёт mu1. Программа зависает.
Как избежать Deadlock:
-
Порядок блокировок: Самый надежный способ — захватывать мьютексы всегда в одном и том же строгом порядке. Если бы обе горутины сначала пытались захватить
mu1, а затемmu2, взаимной блокировки бы не произошло. -
Использование
sync.RWMutex: Если одна из операций только читает данные, можно использовать блокировку на чтение (RLock), которая не конфликтует с другими блокировками на чтение. -
Таймауты: Использовать
sync.Mutex.TryLock()(в Go 1.18+) или реализовать блокировку с таймаутом, чтобы горутина не ждала ресурс вечно. -
Избегать вложенных блокировок: По возможности, старайтесь не захватывать один мьютекс, пока удерживаете другой.
Ответ 18+ 🔞
А, слушай, вот эта штука — deadlock, или взаимная блокировка, если по-пацански. Это когда две или больше горутин так друг друга заблокировали, что все застыли, как идиоты, в вечном ожидании. Каждая ждёт, пока другая отпустит ресурс, а та сама ждёт. Классический пиздец, короче.
Вот тебе наглядный пиздец в коде:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu1, mu2 sync.Mutex
var wg sync.WaitGroup
wg.Add(2)
// Первая горутина: хватает mu1, потом лезет за mu2
go func() {
defer wg.Done()
fmt.Println("Горутина 1: захватывает mu1...")
mu1.Lock()
defer mu1.Unlock()
time.Sleep(100 * time.Millisecond)
fmt.Println("Горутина 1: пытается захватить mu2...")
mu2.Lock()
defer mu2.Unlock()
}()
// Вторая горутина: хватает mu2, потом лезет за mu1
go func() {
defer wg.Done()
fmt.Println("Горутина 2: захватывает mu2...")
mu2.Lock()
defer mu2.Unlock()
time.Sleep(100 * time.Millisecond)
fmt.Println("Горутина 2: пытается захватить mu1...")
mu1.Lock()
defer mu1.Unlock()
}()
wg.Wait() // Сюда, блядь, мы никогда не дойдём, ёпта
fmt.Println("Программа завершена")
}
Смотри, что тут происходит: первая горутина схватила mu1 и тянется к mu2. Вторая, сука, в это же время схватила mu2 и тянется к mu1. И вот они сидят, смотрят друг на друга, как два упыря, и ждут. А ждать им до скончания века. Программа просто повисла, идиотизм!
Как не наступить на эти грабли, чтобы не выглядеть мудаком:
-
Порядок, блядь, порядок! Самый надёжный способ — всегда хватать мьютексы в одном и том же порядке. Если бы обе сначала лезли к
mu1, а потом кmu2, никакого бы пиздеца не было. Просто дисциплина, ебать её в сраку. -
Используй
sync.RWMutex, если можно. Если твоя операция только читает, а не пишет, бери блокировку на чтение (RLock). Их много можно одновременно взять, конфликта не будет. Не будь жлобом. -
Таймауты, ёпта! Не жди вечно. В Go 1.18+ есть
TryLock(), или сам организуй проверку с таймаутом. Если не даётся — отступи, подумай, не будь ослом упёртым. -
Избегай вложенных блокировок, как огня. Старайся не хватать новый мьютекс, пока не отпустил старый. Это как с двумя хуями — хрен одновременно управишься, только deadlock себе устроишь.