Что такое Deadlock (взаимная блокировка) в Go и как его избежать?

Ответ

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:


  1. Порядок блокировок: Самый надежный способ — захватывать мьютексы всегда в одном и том же строгом порядке. Если бы обе горутины сначала пытались захватить mu1, а затем mu2, взаимной блокировки бы не произошло.



  2. Использование sync.RWMutex: Если одна из операций только читает данные, можно использовать блокировку на чтение (RLock), которая не конфликтует с другими блокировками на чтение.



  3. Таймауты: Использовать sync.Mutex.TryLock() (в Go 1.18+) или реализовать блокировку с таймаутом, чтобы горутина не ждала ресурс вечно.



  4. Избегать вложенных блокировок: По возможности, старайтесь не захватывать один мьютекс, пока удерживаете другой.