Что такое deadlock (взаимная блокировка) и каковы условия его возникновения?

Ответ

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

Для возникновения deadlock необходимо одновременное соблюдение четырёх условий (условия Коффмана):

  1. Взаимное исключение (Mutual Exclusion): Ресурс в каждый момент времени может быть занят только одной горутиной.
  2. Удержание и ожидание (Hold and Wait): Горутина удерживает как минимум один ресурс и запрашивает другие ресурсы, которые в данный момент заняты другими горутинами.
  3. Отсутствие вытеснения (No Preemption): Ресурс не может быть принудительно отобран у горутины; он может быть освобожден только той горутиной, которая его захватила.
  4. Кольцевое ожидание (Circular Wait): Существует цепочка из двух или более горутин, где каждая ожидает ресурс, удерживаемый следующей горутиной в цепочке.

Классический пример на Go:

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()
        mu1.Lock()
        fmt.Println("Горутина 1: захватила mu1")
        time.Sleep(100 * time.Millisecond) // Даем время второй горутине захватить mu2
        mu2.Lock()
        fmt.Println("Горутина 1: захватила mu2")
        mu2.Unlock()
        mu1.Unlock()
    }()

    // Горутина 2: захватывает mu2, затем пытается захватить mu1
    go func() {
        defer wg.Done()
        mu2.Lock()
        fmt.Println("Горутина 2: захватила mu2")
        time.Sleep(100 * time.Millisecond)
        mu1.Lock() // <- Здесь произойдет deadlock
        fmt.Println("Горутина 2: захватила mu1")
        mu1.Unlock()
        mu2.Unlock()
    }()

    wg.Wait()
}

Способы предотвращения deadlock:

  • Соблюдение порядка блокировок: Всегда захватывать мьютексы в одном и том же строгом порядке (например, сначала mu1, потом mu2 во всех горутинах).
  • Использование таймаутов: Пытаться захватить блокировку с таймаутом (например, с помощью select и time.After), чтобы избежать вечного ожидания.
  • Использование sync.RWMutex: Позволяет множественные блокировки на чтение, снижая вероятность конфликтов.

Ответ 18+ 🔞

А, слушай, смотри, вот эта хуйня — deadlock, или по-нашему, взаимная блокировка. Это когда твои горутины, такие умные, встали как идиоты в очередь друг за другом и ждут, пока кто-то освободит ресурс. А ресурс-то занят другим таким же ждуном! И все, пиздец, система встала колом, тишина, только процессор тихо посвистывает.

Чтобы этот цирк случился, нужно аж четыре условия, как в дурдоме:

  1. Взаимное исключение. Один ресурс — один хозяин. Как последний пирожок в столовой — либо ты его сожрал, либо я, а вместе херачить не получится.
  2. Удержание и ожидание. Это классика! Горутина, сука, уже вцепилась в один ресурс мёртвой хваткой, а теперь ещё и второй требует, который у другой. Жадная пизда.
  3. Отсутствие вытеснения. Ресурс нельзя просто так отобрать, как игрушку у ребёнка в песочнице. Только тот, кто захватил, тот и должен отпустить. А он не отпускает, блядь!
  4. Кольцевое ожидание. Вот тут начинается настоящий театр абсурда. Первая горутина ждёт ресурс от второй, вторая — от третьей, а третья, гадёныш, ждёт как раз от первой! Замкнутый круг, ёпта! Все сидят и смотрят друг на друга, как дурачки.

Вот тебе наглядный пиздец на Go:

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()
        mu1.Lock()
        fmt.Println("Горутина 1: захватила mu1")
        time.Sleep(100 * time.Millisecond) // Подождать, чтоб вторая успела нагадить
        mu2.Lock() // А тут она обосрётся и зависнет навечно!
        fmt.Println("Горутина 1: захватила mu2")
        mu2.Unlock()
        mu1.Unlock()
    }()

    // Вторая горутина: хватает mu2, потом лезет за mu1
    go func() {
        defer wg.Done()
        mu2.Lock()
        fmt.Println("Горутина 2: захватила mu2")
        time.Sleep(100 * time.Millisecond)
        mu1.Lock() // <- Вот здесь, блядь, и будет наш deadlock! Встретились два упыря.
        fmt.Println("Горутина 2: захватила mu1")
        mu1.Unlock()
        mu2.Unlock()
    }()

    wg.Wait() // И мы тут будем ждать их до второго пришествия, ебать.
}

Как не попасть в эту ловушку для дегенератов?

  • Порядок, сука, порядок! Всегда захватывай мьютексы в одной и той же последовательности. Сначала mu1, потом mu2, и никуда не выёбывайся. Тогда кольца не получится.
  • Таймауты, ёбта! Не жди вечно, поставь deadline. Попробовал захватить мьютекс — не получилось за N миллисекунд — отъебнись и иди другим путём, не упрямься.
  • sync.RWMutex в помощь. Если много читателей, а пишет один — используй эту штуку. Она позволяет многим одновременно читать, что снижает вероятность, что все перегрызутся из-за одной переменной.

Короче, думай головой, когда пишешь многопоточность, а то получишь deadlock и будешь сидеть, как Герасим, который Муму утопил, а потом охуевать от содеянного.