JavaRush /Курсы /C# SELF /Другие проблемы: Livelock и Starvation

Другие проблемы: Livelock и Starvation

C# SELF
57 уровень , 3 лекция
Открыта

1. Введение

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

Это как если бы вы с коллегой в узком коридоре постоянно делали шаг назад, чтобы пропустить друг друга, и из-за этого так и не проходили дальше. Забавно со стороны, но ужасно в программе.

Пример Livelock: программисты в дверях

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

В многопоточной программе это выглядит так: поток видит ресурс занятым, отступает, проверяет ещё раз, снова уступает, и так без конца.

2. Livelock на практике

Давайте реализуем аналогичную ситуацию в коде. Допустим, у нас есть два ресурса (например, два банковских счёта), и два потока одновременно пытаются перевести деньги друг другу, стараясь не «встать в клинч».

class Account
{
    public int Balance { get; set; }
    public object LockObj { get; } = new object();
}

public class Program
{
    static void Transfer(Account from, Account to, int amount)
    {
        while (true)
        {
            bool lockedFrom = false;
            bool lockedTo = false;
            try
            {
                Monitor.TryEnter(from.LockObj, 100, ref lockedFrom); 
                Monitor.TryEnter(to.LockObj, 100, ref lockedTo);

                if (lockedFrom && lockedTo)
                {
                    from.Balance -= amount;
                    to.Balance += amount;
                    break; // Перевод выполнен!
                }
            }
            finally
            {
                if (lockedFrom) Monitor.Exit(from.LockObj);
                if (lockedTo) Monitor.Exit(to.LockObj);
            }
            // Не получилось? Отступим и попробуем снова — вежливо!
            Thread.Sleep(1);
        }
    }
    // ... Запуск двух потоков с Transfer(A, B, ...); Transfer(B, A, ...);
}

В этом примере оба потока многократно пытаются захватить замки. Но если оба всё время видят, что замки заняты, и уступают, то всё сводится к «вежливому» уступанию и ни к какому переводу денег. Полагаться на случайность планировщика — плохая стратегия для надёжной программы. Добавляйте паузы и backoff, не закручивайте короткие «пустые» циклы на Monitor.TryEnter и Thread.Sleep.

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

Deadlock Livelock
Потоки Остановились, ждут Активно крутятся, ждут
Ресурсы Заблокированы навсегда Явно не заблокированы
CPU Почти не грузится Может грузиться на 100%
Решение Таймауты, порядок блоков Случайность, паузы, backoff

3. Starvation (голодание): когда до ресурса не доходит очередь

Starvation (буквально «голодание») — это ситуация, когда один или несколько потоков постоянно обходятся вниманием и не получают доступа к нужному ресурсу, потому что другие потоки всё время оказываются впереди.

В отличие от Deadlock и Livelock, здесь никто не блокирован навсегда и никто бесконечно не уступает — просто некоторым потокам постоянно не достаётся «куска пирога».

Иллюстрация: реал-лайф аналогия

Представьте столовую с двумя очередями: «VIP» (например, касса для сотрудников года) и «для всех остальных». VIP-очередь короткая, но туда всегда кто-то успевает раньше. «Обычные» посетители стоят по полчаса и смотрят, как VIP снова проходят вперёд. Это и есть Starvation!

4. Starvation на C#: практика и отладка

Рассмотрим ситуацию с lock, когда один поток, обладающий большим приоритетом, постоянно успевает войти в критическую секцию, а другой «отстаёт»:

private static readonly object _locker = new object();

static void Greedy()
{
    while (true)
    {
        lock (_locker)
        {
            Console.WriteLine("Жадный поток захватил ресурс...");
            Thread.Sleep(10); // дольше держит замок
        }
        Thread.Sleep(1);
    }
}

static void Poor()
{
    while (true)
    {
        lock (_locker)
        {
            Console.WriteLine("Бедный поток попытался войти...");
        }
    }
}

Если оба потока запустить, то "Greedy" («Жадный») долго держит замок и быстро захватывает его снова, а "Poor" («Бедный») почти не попадает внутрь и фактически «голодает» без ресурса. В реальных задачах starvation может быть менее очевидным: например, если у одного потока ниже приоритет или его чаще вытесняет ОС.

Где встречается starvation?

  • В потоках с низким приоритетом.
  • При неправильной организации очередей задач (нет честной FIFO-политики).
  • В ReaderWriterLockSlim с настройками по умолчанию: если писатель приходит редко, а читатели идут бесконечным потоком, то писатель будет всё время «голодать», так как читатели занимают Read Lock и не дают получить Write Lock.

5. Почему Starvation — не всегда баг, но всегда проблема

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

Как обнаружить и диагностировать Livelock и Starvation

  • Сильно выросло потребление CPU, но задача «стоит на месте» — возможно, у вас Livelock.
  • Потоки вроде работают, но некоторые задачи никогда не завершаются — вероятен Starvation.
  • Логи: если журнал событий показывает, что определённые потоки или задачи никогда не получают доступ к ресурсу — стопроцентное «голодание».

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

  • Не ограничивайтесь попытками захвата ресурсов только через «уступчивые» non-blocking методы типа Monitor.TryEnter. Если не удалось, добавляйте случайные паузы (Thread.Sleep(Random.Next(1, 10)))), чтобы потоки не совпадали по времени.
  • Не используйте постоянные короткие циклы с захватом ресурса — иначе цикл «отступил — попробовал» станет бесконечным.
  • Иногда помогает изменить алгоритм: ввести «право старшего», чтобы только один поток мог уступать, а другой — нет.

Как избежать Starvation

  • Следите за приоритетами потоков: фоновые или приоритетные задачи не должны вечно мешать остальным.
  • Используйте очереди (ConcurrentQueue<T>, каналы, очередь задач) с честной политикой FIFO, чтобы задачи обслуживались по порядку.
  • В ReaderWriterLockSlim можно приостановить новых читателей в пользу писателей, чтобы писатель всё-таки дождался Write Lock.
  • Применяйте таймауты и логи: если поток пытается получить замок слишком долго, выводите предупреждение.
  • Уменьшайте длительность удержания блокировок и разносите критические секции.

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

Сценарий Deadlock Livelock Starvation
Потоки Стоят мёртвым Бегают, но впустую Кто-то работает, кто-то нет
Доступ к ресурсу Нет Нет Неравномерный, иногда нет
CPU Не грузится Грузится сильно Грузится, но не всеми
Критическая ошибка? Да Да Иногда
2
Задача
C# SELF, 57 уровень, 3 лекция
Недоступна
Демонстрация Starvation
Демонстрация Starvation
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ