Что такое 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. Избегать вложенных блокировок: По возможности, старайтесь не захватывать один мьютекс, пока удерживаете другой.

Ответ 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. И вот они сидят, смотрят друг на друга, как два упыря, и ждут. А ждать им до скончания века. Программа просто повисла, идиотизм!

Как не наступить на эти грабли, чтобы не выглядеть мудаком:

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

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

  3. Таймауты, ёпта! Не жди вечно. В Go 1.18+ есть TryLock(), или сам организуй проверку с таймаутом. Если не даётся — отступи, подумай, не будь ослом упёртым.

  4. Избегай вложенных блокировок, как огня. Старайся не хватать новый мьютекс, пока не отпустил старый. Это как с двумя хуями — хрен одновременно управишься, только deadlock себе устроишь.