Что такое 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: Позволяет множественные блокировки на чтение, снижая вероятность конфликтов.