Что такое Livelock (живая блокировка) и чем он отличается от Deadlock?

Ответ

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

Классическая аналогия: два человека встречаются в узком коридоре. Оба пытаются уступить дорогу, шагая в одну и ту же сторону, затем в другую, и так до бесконечности. Они оба активны, но с места не сдвигаются.

Сравнение Deadlock и Livelock

Характеристика Deadlock (Мертвая блокировка) Livelock (Живая блокировка)
Состояние потоков Заблокированы (ожидают ресурс) Активны (выполняют бесполезную работу)
Потребление CPU Практически нулевое Высокое, так как потоки постоянно работают
Пример Две горутины захватили по одному мьютексу и ждут мьютекс, захваченный другой. Две горутины пытаются захватить два мьютекса, но при неудаче освобождают свой и пробуют снова, постоянно мешая друг другу.

Пример Livelock в Go

Представим двух "вежливых" людей, которые пытаются воспользоваться одной ложкой. Каждый раз, когда один видит, что другой тоже хочет ложку, он уступает.

package main

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

// Общий ресурс
type Spoon struct{
    mu sync.Mutex
    owner string
}

func (s *Spoon) PickUp(name string) {
    s.mu.Lock()
    s.owner = name
}

func (s *Spoon) PutDown() {
    s.owner = ""
    s.mu.Unlock()
}

func main() {
    spoon := &Spoon{}
    var wg sync.WaitGroup
    wg.Add(2)

    // Два "вежливых" едока
    go func() {
        defer wg.Done()
        name := "Alice"
        for i := 0; i < 5; { // Пытаемся поесть 5 раз
            spoon.PickUp(name)
            // Проверяем, не пытается ли другой взять ложку
            if spoon.owner != name { // Другой успел перехватить
                fmt.Printf("%s видит, что ложку взял другой, и уступает.n", name)
                spoon.PutDown()
                continue
            }

            fmt.Printf("%s ест.n", name)
            time.Sleep(100 * time.Millisecond)
            spoon.PutDown()
            i++
        }
    }()

    go func() {
        defer wg.Done()
        name := "Bob"
        for i := 0; i < 5; {
            spoon.PickUp(name)
            if spoon.owner != name {
                fmt.Printf("%s видит, что ложку взял другой, и уступает.n", name)
                spoon.PutDown()
                continue
            }

            fmt.Printf("%s ест.n", name)
            time.Sleep(100 * time.Millisecond)
            spoon.PutDown()
            i++
        }
    }()

    wg.Wait()
    fmt.Println("Обед закончен.")
}

В этом примере, если обе горутины будут работать синхронно, они могут постоянно захватывать и тут же освобождать мьютекс, не давая друг другу поесть. Они активны, но прогресса нет.

Как избежать Livelock?

  • Введение случайности: Добавление случайной задержки перед повторной попыткой (exponential backoff with jitter) может помочь потокам "разойтись" во времени.
  • Четкий порядок: Установление строгого порядка захвата ресурсов, как и в случае с Deadlock.

Ответ 18+ 🔞

А, ну это же классика, ёпта! Ситуация, когда все вроде как шевелятся, а толку — нихуя. Живая блокировка, Livelock, называется.

Представь, блядь, двух вежливых мудаков в узком коридоре. Один такой: «О, проходи, проходи!» — и шаг в сторону. А второй, тоже не промах: «Да нет, ты первый, я постою!» — и шаг в ту же самую сторону. И они так, сука, синхронно туда-сюда пляшут, потеют, улыбаются, а продвинуться ни на сантиметр не могут. Вот это и есть livelock, блядь. Все в работе, CPU грузится, а прогресс — ноль, пиздец.

Deadlock vs Livelock: кратко и с матом

Признак Deadlock (Мёртвый) Livelock (Живой, но идиотский)
Состояние Все стоят как вкопанные и ждут. Все бегают, суетятся, но нахуй никуда не прибегают.
Загрузка CPU Ноль, тишина, покой. Овердохуища, все ядра пыхтят.
Пример Два потока схватили по мьютексу и ждут, пока другой отпустит свой. Два потока пытаются захватить два мьютекса, но если не получается, отпускают свои и начинают сначала, вечно мешая друг другу.

Пример на Go: «Вежливые обжоры»

Смотри, вот два чудика, Алиса и Боб, хотят суп одной ложкой есть. Такие воспитанные, блядь.

package main

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

// Общая ложка, главный яблоко раздора
type Spoon struct{
    mu sync.Mutex
    owner string
}

func (s *Spoon) PickUp(name string) {
    s.mu.Lock()
    s.owner = name
}

func (s *Spoon) PutDown() {
    s.owner = ""
    s.mu.Unlock()
}

func main() {
    spoon := &Spoon{}
    var wg sync.WaitGroup
    wg.Add(2)

    // Алиса, дура вежливая
    go func() {
        defer wg.Done()
        name := "Alice"
        for i := 0; i < 5; { // Пытается 5 раз поесть, хы
            spoon.PickUp(name)
            // Опа, а вдруг Боб тоже хочет? Надо уступить, хороший тон!
            if spoon.owner != name { // Ага, блядь, уже не её!
                fmt.Printf("%s видит, что ложку взял другой, и уступает.n", name)
                spoon.PutDown()
                continue // И по новой, ёбта!
            }

            fmt.Printf("%s ест.n", name)
            time.Sleep(100 * time.Millisecond)
            spoon.PutDown()
            i++ // Ура, один разок поел!
        }
    }()

    // Боб, второй вежливый мудак
    go func() {
        defer wg.Done()
        name := "Bob"
        for i := 0; i < 5; {
            spoon.PickUp(name)
            if spoon.owner != name {
                fmt.Printf("%s видит, что ложку взял другой, и уступает.n", name)
                spoon.PutDown()
                continue
            }

            fmt.Printf("%s ест.n", name)
            time.Sleep(100 * time.Millisecond)
            spoon.PutDown()
            i++
        }
    }()

    wg.Wait()
    fmt.Println("Обед закончен.")
}

Если они синхронно попадут, то будет пиздец. Схватит Алиса ложку, а Боб тут как тут — она её бросит. Схватит Боб — она уже опять тянется, он тоже бросает. И так до второго пришествия, в рот меня чих-пых! Оба активны, оба «работают», но суп холодный останется.

Как не словить эту хуйню?

  • Добавить хаоса, блядь. Сделать случайную паузу перед повторной попыткой. Пусть один подождёт чуть дольше — тогда второй успеет всё сделать. Это как в драке — кто первый выждал, тот и прав.
  • Жёсткая иерархия. Чётко договориться, кто и в каком порядке ресурсы хватает. Как в армии: сначала старший по званию, потом младший. Никакой вежливости, только жёсткий порядок, ебать.

Короче, livelock — это когда все слишком стараются быть хорошими и умными, а в итоге получается ебаный цирк с конями. Иногда надо быть проще и наглее, и тогда всё заведётся.