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

Ответ

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

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

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

Классический пример на Swift (с NSLock):

let lockA = NSLock()
let lockB = NSLock()

DispatchQueue.global().async {
    lockA.lock()          // Поток 1 захватывает lockA
    sleep(1)              // Имитация работы
    lockB.lock()          // Поток 1 ЖДЁТ lockB (но его держит Поток 2)
    // Критическая секция...
    lockB.unlock()
    lockA.unlock()
}

DispatchQueue.global().async {
    lockB.lock()          // Поток 2 захватывает lockB
    sleep(1)              // Имитация работы
    lockA.lock()          // Поток 2 ЖДЁТ lockA (но его держит Поток 1)
    // Критическая секция...
    lockA.unlock()
    lockB.unlock()
}
// Оба потока заблокированы навсегда.

Способы предотвращения:

  • Упорядочивание блокировок: Всегда захватывать ресурсы в одинаковом глобальном порядке (сначала A, потом B).
  • Использование NSLock.try() или блокировок с таймаутом.
  • Отказ от явных блокировок в пользу высокоуровневых механизмов GCD (очереди, DispatchSemaphore, DispatchGroup).
  • Проектирование без состояний или с использованием потокобезопасных структур данных.

Ответ 18+ 🔞

Да ты посмотри, какая дичь может случиться в коде! Взаимная блокировка, или deadlock, если по-ихнему. Это когда твои потоки, эти ебушки-воробушки, встают в позу и намертво друг друга ждут. Как два упыря в подъезде: один держит дверь, другой — ключ, и оба нихуя не двигаются, потому что каждый ждёт, пока второй отвалит.

Чтобы эта пиздопроебибна случилась, нужно аж четыре условия, как в плохом анекдоте. Условия Коффмана, блядь, звучат.

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

Вот смотри, как это выглядит в коде, прям классика жанра:

let lockA = NSLock()
let lockB = NSLock()

DispatchQueue.global().async {
    lockA.lock()          // Первый чувак схватил замок А
    sleep(1)              // Подумал, почесал
    lockB.lock()          // Пытается взять замок Б, но он уже у второго! Ждёт...
    // А сюда он никогда не попадёт, блядь
    lockB.unlock()
    lockA.unlock()
}

DispatchQueue.global().async {
    lockB.lock()          // Второй чувак сразу цапнул замок Б
    sleep(1)              // Тоже подумал
    lockA.lock()          // Тянется за замком А, а он у первого! Тоже ждёт...
    // И здесь — тишина, вечный покой
    lockA.unlock()
    lockB.unlock()
}
// Итог: два потока, два замка и нихуя не происходит. Накрылись медным тазом.

Ну и как с этой хуйней бороться? А вот как, слушай сюда:

  • Порядок, сука, порядок! Самый надёжный способ. Всегда договаривайся брать ресурсы в одной и той же последовательности. Сначала всегда lockA, потом уже lockB. Тогда циклического ожидания не будет — второй поток просто подождёт, пока первый всё отпустит, и спокойно возьмёт оба по порядку.
  • Попробуй, но не настаивай. Используй try() или блокировки с таймаутом. Не получилось взять второй ресурс — отпусти первый, подожди немного и попробуй снова. Не надо упорствовать, как Герасим с Муму.
  • Не выёбывайся с низкоуровневыми замками. GCD, блядь, для чего придумали? Используй очереди, семафоры, группы — там много умных штук, которые помогают эту проблему обойти.
  • А может, вообще без этого? Самый крутой способ — спроектировать так, чтобы не было общих изменяемых состояний. Идеал, конечно, но стремиться к нему надо.

Вот так вот, блядь. А то напишешь такой deadlock, а потом сиди и думай, почему приложение виснет, а терпения ебать ноль.