Что такое deadlock (взаимная блокировка) в Go?

Ответ

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

Это приводит к полной остановке выполнения задействованных горутин. Среда выполнения Go способна обнаруживать такие ситуации и аварийно завершать программу с выводом состояния всех горутин, что помогает в отладке.

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

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu1, mu2 sync.Mutex

    go func() {
        fmt.Println("Горутина 1: пытается захватить mu1")
        mu1.Lock()
        fmt.Println("Горутина 1: захватила mu1")
        time.Sleep(100 * time.Millisecond)

        fmt.Println("Горутина 1: пытается захватить mu2")
        mu2.Lock() // Бесконечно ждёт mu2, которую удерживает Горутина 2
        defer mu2.Unlock()
        defer mu1.Unlock()
    }()

    go func() {
        fmt.Println("Горутина 2: пытается захватить mu2")
        mu2.Lock()
        fmt.Println("Горутина 2: захватила mu2")
        time.Sleep(100 * time.Millisecond)

        fmt.Println("Горутина 2: пытается захватить mu1")
        mu1.Lock() // Бесконечно ждёт mu1, которую удерживает Горутина 1
        defer mu1.Unlock()
        defer mu2.Unlock()
    }()

    time.Sleep(2 * time.Second) // Даём горутинам время на deadlock
    fmt.Println("Программа завершена (эта строка не будет достигнута)")
}

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

  1. Соблюдение порядка блокировок (Lock Ordering): Всегда захватывать мьютексы в строго определенном порядке. Например, сначала mu1, потом mu2 во всех горутинах. Это исключает возможность циклического ожидания.
  2. Использование sync.RWMutex: Позволяет множественные блокировки на чтение, снижая вероятность конфликтов, если операции в основном читающие.
  3. Применение таймаутов: Использовать select с time.After при работе с каналами или sync.Mutex.TryLock (появился в Go 1.18) для попытки захвата мьютекса без блокировки.
  4. Использование каналов: Вместо мьютексов и общего состояния, можно организовать коммуникацию через каналы, передавая владение данными от одной горутины к другой. Это один из идиоматических подходов в Go.