Что такое Deadlock (взаимная блокировка)?

Ответ

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

Классический пример на C#:

object lockA = new object();
object lockB = new object();

Thread thread1 = new Thread(() => {
    lock (lockA) {
        Thread.Sleep(100);
        // Поток 1 ждёт lockB, который удерживается потоком 2
        lock (lockB) {
            Console.WriteLine("Thread1 completed");
        }
    }
});

Thread thread2 = new Thread(() => {
    lock (lockB) {
        Thread.Sleep(100);
        // Поток 2 ждёт lockA, который удерживается потоком 1
        lock (lockA) {
            Console.WriteLine("Thread2 completed");
        }
    }
});

thread1.Start();
thread2.Start();
// Оба потока зависнут.

Условия возникновения (Коффмана):

  1. Взаимное исключение: ресурс не может использоваться более чем одним потоком одновременно.
  2. Удержание и ожидание: поток удерживает один ресурс и ждёт другой.
  3. Отсутствие вытеснения: ресурс нельзя отобрать у потока, только добровольно освободить.
  4. Циклическое ожидание: потоки образуют замкнутый цикл ожидания ресурсов.

Стратегии предотвращения:

  • Упорядочивание блокировок: Всегда захватывать блокировки в строго определённом глобальном порядке (например, сначала lockA, потом lockB).
  • Использование таймаутов: Monitor.TryEnter(lockObj, TimeSpan) или SemaphoreSlim.Wait(TimeSpan) для выхода из ожидания.
  • Уменьшение времени удержания блокировки: Выполнять внутри lock только минимально необходимые операции.
  • Проектирование без блокировок: Использование потокобезопасных коллекций (ConcurrentBag, ConcurrentDictionary), async/await для I/O операций или модели акторов.
  • Статический анализ: Инструменты вроде Microsoft.ConcurrencyVisualizer или Roslyn-анализаторы для поиска потенциальных deadlock'ов.

Ответ 18+ 🔞

Давайте разберем эту хитрую хуйню под названием deadlock. Представь, что два потока устроили друг друху вечную пьянку в ожидании, и оба намертво повисли, как два мудака в подъезде, которые пропускают друг друга в дверях. Никто не хочет уступить, и оба стоят до скончания времён.

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

object lockA = new object();
object lockB = new object();

Thread thread1 = new Thread(() => {
    lock (lockA) {
        Thread.Sleep(100);
        // Поток 1 ждёт lockB, который удерживается потоком 2
        lock (lockB) {
            Console.WriteLine("Thread1 completed");
        }
    }
});

Thread thread2 = new Thread(() => {
    lock (lockB) {
        Thread.Sleep(100);
        // Поток 2 ждёт lockA, который удерживается потоком 1
        lock (lockA) {
            Console.WriteLine("Thread2 completed");
        }
    }
});

thread1.Start();
thread2.Start();
// Оба потока зависнут.

Видишь эту ебучую схему? Первый поток схватил lockA и тянется к lockB. Второй, как хитрая жопа, уже прихватил lockB и хочет lockA. И вот они сидят, смотрят друг на друга пустыми глазами, и терпения ебать ноль. Это и есть deadlock, ёпта.

Чтобы эта поебень вообще могла случиться, должны сойтись четыре звёзды, точнее, условия Коффмана:

  1. Взаимное исключение. Ресурс — как последняя пачка сигарет в деревне, его может держать только один поток. Остальные ждут, как лохи.
  2. Удержание и ожидание. Поток не просто ждёт, он жадный уёбок: одной лапой держит свой ресурс, а второй тянется к чужому.
  3. Отсутствие вытеснения. У потока нельзя просто так отобрать его ресурс, как у ребёнка конфету. Только если он сам, воспитанный, отпустит.
  4. Циклическое ожидание. Потоки выстроились в круг и каждый ждёт следующего, как идиоты на карусели, которая не крутится.

Теперь, как не попасть в эту ебучую ситуацию? Есть несколько рабочих способов:

  • Упорядочивание блокировок. Самый надёжный способ, блядь. Договорись с собой и всегда захватывай замки в одном и том же порядке. Например, сначала всегда lockA, а потом уже lockB. Тогда цикла не получится, один из потоков просто подождёт, пока другой всё сделает, и всё будет пиздато.
  • Использование таймаутов. Не жди вечно, как лох! Используй Monitor.TryEnter(lockObj, TimeSpan). Не дали замок за отведённое время — пошёл нахуй, освободи что держишь и попробуй позже, или вообще откатись. Это как стучаться в дверь: постучал-постучал, не открывают — пошёл в бар.
  • Уменьшение времени удержания блокировки. Не будь той бабой на кассе, которая расплачивается мелочью и ищет сдачу по всем карманам. Делай внутри lock только самое необходимое, а всю остальную ебучую логику — за его пределами. Быстро взял, быстро отпустил.
  • Проектирование без блокировок. Вообще, блядь, высший пилотаж. Используй потокобезопасные коллекции вроде ConcurrentDictionary, или async/await для операций ввода-вывода. Меньше возни с замками — меньше шансов нихуя не сойтись.
  • Статический анализ. Это как презерватив для кода. Используй всякие анализаторы, которые могут заранее сказать: "чувак, тут у тебя потенциальный deadlock нарисовался, ты в курсе?". Например, Microsoft.ConcurrencyVisualizer — полезная штука, хоть и не панацея.

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

Видео-ответы