Livelock и голодание (starvation)

Не всякий «зависший» поток стоит на месте: иногда он бешено работает, но прогресса нет.

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

Deadlock — это «оба замерли». Livelock — это «оба суетятся, но без толку». Представьте двух людей в узком коридоре: каждый вежливо отходит в ту же сторону, что и другой, они снова сталкиваются, снова отходят — и так бесконечно. Никто не заблокирован, но никто и не проходит.

Откуда берётся livelock

Часто livelock рождается из наивной попытки избежать deadlock: «если не смог взять вторую блокировку — отпущу первую и попробую снова». Если оба потока делают это синхронно, они вечно отпускают и захватывают по кругу.

Поток A: взял l1, не смог l2 -> отпустил l1, ждёт, повторил
Поток B: взял l2, не смог l1 -> отпустил l2, ждёт, повторил
(и так до бесконечности — оба активны, прогресса нет)

Лечится добавлением случайной задержки перед повтором: тогда потоки рассинхронизируются, и один проскочит первым.

Голодание

Голодание — когда поток постоянно «оттесняют». Например, при строго приоритетном планировании высокоприоритетные потоки идут без конца, а низкоприоритетный не получает ядра никогда. Или «писатель» не может войти, потому что поток читателей не иссякает.

import random
random.seed(7)

# моделируем backoff: случайная пауза рассинхронизирует попытки
for attempt in range(1, 6):
    backoff = round(random.uniform(0, 1), 2)
    print(f"попытка {attempt}: жду {backoff} перед повтором")

Вывод:

попытка 1: жду 0.32 перед повтором
попытка 2: жду 0.15 перед повтором
попытка 3: жду 0.65 перед повтором
попытка 4: жду 0.07 перед повтором
попытка 5: жду 0.54 перед повтором

Как работает под капотом

Livelock не виден детектору deadlock: потоки не заблокированы, граф ожидания пуст, процессор загружен. Поэтому livelock диагностировать сложнее — программа «жива», но не прогрессирует. Против голодания планировщики применяют старение (aging): чем дольше поток ждёт, тем выше его приоритет, и рано или поздно он получает ядро. Очереди по принципу FIFO тоже гарантируют справедливость.

Частые ошибки

  • Одинаковый фиксированный backoff у всех. Потоки остаются синхронными — livelock не уходит.
  • Жёсткие приоритеты без старения. Низкоприоритетные потоки голодают.
  • Считать высокую загрузку CPU признаком прогресса. При livelock CPU занят, а работа не делается.

Итог

  • Deadlock — потоки замерли; livelock — потоки активны, но не продвигаются.
  • Голодание — поток никогда не получает ресурс.
  • Случайный backoff лечит livelock, рассинхронизируя повторы.
  • Старение и справедливые очереди спасают от голодания.
Проверьте себя
1. Чем livelock отличается от deadlock?
AЭто одно и то же
BПри livelock потоки активны и что-то делают, но не продвигаются к цели
CLivelock происходит только на одном ядре
DПри livelock потоки полностью остановлены
2. Что помогает разорвать livelock при повторных попытках захвата блокировок?
AОдинаковая фиксированная задержка у всех потоков
BСлучайная задержка (backoff) перед повтором
CПолное удаление блокировок
DПовышение приоритета всех потоков сразу
3. Какой механизм планировщика спасает поток от голодания?
AСтарение (aging) — рост приоритета с увеличением времени ожидания
BУдаление потока
CОтключение других ядер
DБесконечное вытеснение