Что такое 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.