Ответ
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.