← Назад к вопросам

Что такое Live Lock?

2.7 Senior🔥 62 комментариев
#Конкурентность и горутины

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое Live Lock?

Live Lock (или "живая блокировка") — это состояние многопоточной или распределённой системы, в котором процессы или потоки активно выполняются, но не могут продвинуться в выполнении своей полезной работы из-за постоянного взаимодействия друг с другом, ведущего к бесконечному циклу попыток разрешить конфликт. В отличие от Dead Lock ("взаимной блокировки"), где потоки полностью замирают в ожидании ресурсов, при Live Lock они продолжают "жить" — потреблять процессорное время и выполнять операции, но без прогресса.

Ключевые характеристики Live Lock

  • Активная работа: Потоки не блокируются (не переходят в состояние waiting или sleeping), а находятся в состоянии running или runnable.
  • Отсутствие прогресса: Несмотря на активность, полезная задача (например, запись данных, завершение транзакции) не выполняется.
  • Циклическая зависимость: Действия одного потока провоцируют ответные действия другого, которые, в свою очередь, снова активируют первого, создавая бесконечный цикл.
  • Часто связано с логикой разрешения конфликтов: Live Lock часто возникает при попытке "вежливо" разрешить тупиковую ситуацию (например, при обнаружении потенциального Dead Lock).

Классический пример на Go

Рассмотрим сценарий с двумя горутинами, пытающимися "вежливо" пропустить друг друга через общий "коридор" (разделяемый ресурс), используя механизм отката (backoff).

package main

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

type SharedSpace struct {
	mu    sync.Mutex
	owner string
}

func (s *SharedSpace) tryEnter(name string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.owner == "" || s.owner == name {
		s.owner = name
		return true // Успешно вошли или уже владеем
	}
	return false // Занято другим
}

func (s *SharedSpace) leave(name string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.owner == name {
		s.owner = ""
	}
}

func worker(name string, space *SharedSpace, wg *sync.WaitGroup) {
	defer wg.Done()

	for i := 0; i < 3; i++ { // Попробуем выполнить работу 3 раза
		attempts := 0
		for !space.tryEnter(name) {
			attempts++
			// "Вежливая" логика: если не получается, уступаем и пробуем снова
			time.Sleep(time.Duration(attempts) * 10 * time.Millisecond)
			// Здесь может возникнуть Live Lock: обе горутины отступают
			// и синхронизируются так, что постоянно мешают друг другу.
			fmt.Printf("%s: жду, попытка %d\n", name, attempts)
			if attempts >-supported5 { // Защита от бесконечного цикла в примере
				fmt.Printf("%s: сдаюсь на этой итерации\n", name)
				return
			}
		}

		// Критическая секция (полезная работа)
		fmt.Printf("%s: выполняю работу %d\n", name, i)
		time.Sleep(50 * time.Millisecond) // Имитация работы

		space.leave(name)
		time.Sleep(time.Duration(attempts) * 5 * time.Millisecond) // Задержка после работы
	}
}

func main() {
	var wg sync.WaitGroup
	space := &SharedSpace{}

	wg.Add(2)
	go worker("Горутина-A", space, &wg)
	go worker("Горутина-B", space, &wg)

	wg.Wait()
	fmt.Println("Программа завершена.")
}

Что здесь может произойти?

  1. Горутина-A захватывает space.
  2. Горутина-B пытается захватить, получает false, отступает (Sleep).
  3. Горутина-A завершает работу, освобождает ресурс и также делает Sleep.
  4. Горутина-B просыпается, пытается захватить — успешно.
  5. Горутина-A просыпается, пытается захватить, получает false, отступает...
  6. Если времена Sleep синхронизируются, горутины могут бесконечно "танцевать" — одна входит, когда другая спит, и наоборот, никогда не завершая все 3 итерации вовремя или делая это крайне медленно. Это и есть Live Lock.

Как отличить Live Lock от Dead Lock?

КритерийDead LockLive Lock
Состояние потоковЗаблокированы (waiting, часто на мьютексе или канале).Активны (running), выполняют код.
Потребление CPUНулевое или очень низкое (ожидание).Высокое (бесполезные циклы).
ПричинаЦиклическое ожидание ресурсов (A ждёт B, B ждёт A)."Излишняя вежливость", некорректная логика повтора.
ДиагностикаЛегче обнаружить (профилировщик покажет блокировки).Сложнее (профилировщик покажет высокую нагрузку на функции разрешения конфликтов).

Методы предотвращения и решения в Go

  1. Детерминированный порядок захвата ресурсов — как и для Dead Lock. Избегайте циклических зависимостей.
  2. Использование таймаутов — примитивы sync не поддерживают таймауты напрямую, но можно использовать select с time.After и каналами или context.Context.
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    defer cancel()
    select {
    case <-semaphore: // Попытка захвата семафора через канал
        // Работа
        semaphore <- struct{}{} // Освобождение
    case <-ctx.Done():
        // Таймаут, отказ от операции или переход к альтернативной логике
    }
    
  3. Экспоненциальная откладка (Exponential Backoff) со случайной добавкой (Jitter) — чтобы циклы разных горутин не синхронизировались.
    backoff := time.Duration(attempts*attempts) * baseDelay
    jitter := time.Duration(rand.Int63n(int64(jitterMax))) // Добавляем случайность
    time.Sleep(backoff + jitter)
    
  4. Использование примитивов более высокого уровня — такие как sync/errgroup, ограниченные рабочие пулы, шаблон "Worker Pool" или очереди (channels с буферами), которые структурно исключают конфликты.
  5. Приоритизация — одна из сторон конфликта должна получить неоспоримое преимущество после нескольких неудач.

Вывод

Live Lock — коварная проблема, маскирующаяся под рабочую нагрузку. Для разработчика на Go важно не только избегать классических Dead Lock, но и проектировать логику повторных попыток и координации горутин так, чтобы система гарантированно продвигалась вперёд, даже в условиях конкуренции. Использование таймаутов, случайных задержек и проверенных высокоуровневых паттернов — лучшая защита от "живой блокировки".