JavaRush /Курсы /C# SELF /Предотвращение и устранение Deadlock

Предотвращение и устранение Deadlock

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

1. Классические условия Кофмана

В 1971 году компьютерный учёный Эдвард Кофман (Edward G. Coffman Jr.) описал четыре условия, без которых deadlock невозможен. Эти правила давно стали «азбукой» для собеседований и курсов по многопоточности. Их полезно знать не только наизусть, но и понимать, зачем они нужны:

  1. Взаимное исключение (Mutual Exclusion): хотя бы один ресурс может быть захвачен только одним потоком одновременно.
  2. Удержание и ожидание (Hold and Wait): поток, захвативший один ресурс, может ждать другие ресурсы.
  3. Неотъемлемость (No Preemption): ресурс нельзя отобрать у потока насильно — только сам поток может его освободить.
  4. Круговое ожидание (Circular Wait): есть цепочка потоков, где каждый ждёт ресурс, который держит следующий.

Если выполняются все четыре условия, deadlock возможен. Если нарушить хотя бы одно — вероятность взаимной блокировки исчезает.

Быстрое принятие решений

flowchart TD
    A[Нужно заблокировать несколько ресурсов?] -- Нет --> B[Стандартный lock]
    A -- Да --> C[Можно ли всегда брать в одном порядке?]
    C -- Да --> D[Всегда берём в одном порядке]
    C -- Нет --> E[Избегайте вложенных lock-ов]
    D --> F[Минимизируйте время под замком]
    E --> F
    B --> F
    F --> G{Всё равно страшно?}
    G -- Да --> H[Добавьте тайм-ауты и повторные попытки]
    G -- Нет --> I[Спите спокойно!]

2. Основные стратегии предотвращения Deadlock

Раз deadlock — это неотъемлемая часть многопоточного мира, наша задача — научиться с ним сосуществовать. В этом помогают разные стратегии: от простых до хитрых.

Всегда блокируйте ресурсы в одном и том же порядке

Суть простая: если все потоки всегда захватывают ресурсы в одном и том же порядке, то круговое ожидание просто не сможет возникнуть. Этот приём часто называют ordered locking — «упорядоченная блокировка».

Пример

Опасный код:


// Поток 1
lock (resA)
{
    lock (resB)
    {
        // ...
    }
}

// Поток 2
lock (resB)
{
    lock (resA)
    {
        // ...
    }
}

Здесь возможен deadlock: первый поток захватил resA и ждёт resB, второй — наоборот. Они оба висят и никогда не разблокируются.

Правильный код:


// Всегда: сначала resA, потом resB (никогда наоборот)
lock (resA)
{
    lock (resB)
    {
        // ...
    }
}

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

Типичная ошибка: если в коде найдётся хоть одно место, где порядок нарушен, риск deadlock возвращается. Поэтому аккуратнее!

Избегайте блокировки нескольких ресурсов одновременно

Это проще всего: если можно не брать замки сразу на два и более ресурса — не берите! Чем больше вложенных блокировок, тем выше риск застрять. Вместо этого можно, например, копировать нужные данные во временные переменные, освободить замок и только тогда работать с другими ресурсами.

Используйте тайм-ауты при захвате замка

Если уж очень нужно взять несколько блокировок, выручат тайм-ауты. Например, вместо обычного lock используйте методы, позволяющие «попробовать» взять замок, а если не удалось — аккуратно всё вернуть назад, например Monitor.TryEnter.


object resourceA = new object();
object resourceB = new object();

bool successA = false, successB = false;
try
{
    // Пытаемся взять оба замка максимум за 2 секунды
    successA = Monitor.TryEnter(resourceA, TimeSpan.FromSeconds(2));
    if (!successA) return; // не получилось, выходим

    successB = Monitor.TryEnter(resourceB, TimeSpan.FromSeconds(2));
    if (!successB) return;

    // Критическая секция
}
finally
{
    if (successB) Monitor.Exit(resourceB);
    if (successA) Monitor.Exit(resourceA);
}

Если второй замок не удаётся взять за 2 секунды, мы разжимаем первый и выходим. В итоге deadlock не произойдёт, а просто кое-какие потоки откатят свои попытки назад.

Минимизируйте объём кода внутри блокировки

Чем меньше участок кода «под замком», тем меньше времени другая ветка будет ожидать. Не стоит делать сложные вычисления, сетевые вызовы и операции с диском внутри критической секции. Захватили замок — быстро изменили общий ресурс — отпустили. Всё остальное делайте снаружи.

Разделяйте состояние — избегайте общих ресурсов

Иногда лучше не бороться с блокировками, а просто избегать общего состояния:

  • Используйте неизменяемые объекты (immutable).
  • Работайте с копиями или локальными переменными вместо глобальных.
  • Передавайте данные сообщениями (Actor Model, очередь сообщений).
  • Используйте потокобезопасные коллекции: ConcurrentDictionary, ConcurrentQueue и др. из System.Collections.Concurrent.

3. Разрешение Deadlock, если уже случилось

Вручную убить и перезапустить (не лучший вариант)

Самый простой способ — завершить все заблокированные процессы. Однако это несёт риск потери данных. Это крайний случай, к которому лучше не доводить.

Обнаружение Deadlock в процессе работы

Некоторые системы позволяют обнаружить deadlock автоматически. Если поток не может взять замок слишком долго, он может логировать событие, уведомлять мониторинг и инициировать восстановление.


if (!Monitor.TryEnter(resource, TimeSpan.FromSeconds(10)))
{
    Console.WriteLine("Похоже, мы попали в deadlock или кто-то держит замок слишком долго!");
    // Можно инициировать восстановление, вывести дамп или уведомить пользователя
}

В распределённых системах используются отдельные детекторы, которые анализируют граф ожидания и принудительно разблокируют зависшие транзакции.

Автоматический откат и повторная попытка

Удобная практика — «сдаваться» и пробовать снова: если не удалось взять все блокировки за разумное время, откатываемся, ждём немного (часто — случайное время для разведения попыток) и повторяем.


for (int attempt = 0; attempt < 3; attempt++)
{
    bool gotRes1 = Monitor.TryEnter(res1, TimeSpan.FromSeconds(2));
    bool gotRes2 = false;
    try
    {
        if (gotRes1)
        {
            gotRes2 = Monitor.TryEnter(res2, TimeSpan.FromSeconds(2));
            if (gotRes2)
            {
                // Критическая секция
                break;
            }
        }
    }
    finally
    {
        if (gotRes2) Monitor.Exit(res2);
        if (gotRes1) Monitor.Exit(res1);
    }

    // Не получилось — ждем и пробуем снова
    Thread.Sleep(500 + new Random().Next(500));
}

4. Лучшие практики и рекомендации

  • Анализируйте порядок захвата замков. Соберите точки входа и проверьте порядок.
  • По возможности используйте коллекции из System.Collections.Concurrent.
  • Применяйте инструменты анализа кода. IDE вроде Rider или Visual Studio помогают находить вложенные блокировки.
  • Логируйте длительные попытки захвата.
  • Проводите стресс‑тестирование многопоточных компонентов.
  • Документируйте соглашения о порядке блокировок. Комментарии спасают от нарушений упорядоченности.

5. Практические примеры

Пример: База данных

В СУБД deadlock — частый гость: две транзакции обновляют одни и те же таблицы, но в разном порядке (A→B и B→A). Большинство СУБД умеют детектировать такие ситуации и «убивают» одну из транзакций.

Пример: Асинхронные методы

В .NET, если смешивать асинхронность и блокировки, можно получить взаимную блокировку: await блокирует поток, удерживающий замок, а другой поток ждёт тот же ресурс. Планируйте границы критических секций и избегайте ожиданий внутри них.

6. Типичные ошибки при работе с блокировками

Ошибка №1: блокировка строк.
lock (myString) — плохая идея. В .NET строки интернированы, поэтому вы фактически блокируете глобальную таблицу строк.

Ошибка №2: разный порядок захвата объектов.
Если в разных местах программы блокировки берутся в разном порядке, вероятность deadlock резко возрастает.

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

2
Задача
C# SELF, 57 уровень, 2 лекция
Недоступна
Реализация автоматического отката при обнаружении Deadlock
Реализация автоматического отката при обнаружении Deadlock
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ