Какое минимальное количество потоков необходимо для возникновения взаимной блокировки (deadlock)?

Ответ

Для возникновения deadlock достаточно двух потоков.

Условия взаимной блокировки (Coffman conditions):

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

Пример deadlock на Swift с двумя потоками:

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

DispatchQueue.global().async {
    lockA.lock()
    sleep(1) // Имитация работы с ресурсом A
    // Поток 1 ждет, пока освободится lockB
    lockB.lock() // БЛОКИРОВКА: lockB удерживается потоком 2
    // Критическая секция...
    lockB.unlock()
    lockA.unlock()
}

DispatchQueue.global().async {
    lockB.lock()
    sleep(1) // Имитация работы с ресурсом B
    // Поток 2 ждет, пока освободится lockA
    lockA.lock() // БЛОКИРОВКА: lockA удерживается потоком 1
    // Критическая секция...
    lockA.unlock()
    lockB.unlock()
}
// Оба потока заблокированы навсегда.

Как избежать: устанавливать строгий порядок захвата блокировок или использовать методы вроде NSLock.try().

Ответ 18+ 🔞

А, слушай, смотри какая история! Ну классика жанра, блядь, дедлок. Ты думаешь, для полного пиздеца нужно полсотни потоков? Хуй там! Достаточно всего двух, ёпта. Двух ебланов, которые друг другу дорогу не уступят.

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

  1. Взаимное исключение: Это когда каждый ресурс — как последняя булка в магазине. Один взял — второй стоит и слюной истекает, пока первый не отдаст. В коде это NSLock, сука.
  2. Удержание и ожидание: Представь: один мудила схватил булку, а второй — последнюю пачку масла. И оба, блядь, друг у друга эту хуйню выпрашивают: «Дай булку!» — «А ты мне масло!». И стоят. Вот это оно и есть.
  3. Отсутствие вытеснения: Ну, отобрать насильно нельзя, блядь. Только если сам отпустит. А он не отпустит, потому что ждёт, пока ему вторую хуйню поднесут.
  4. Циклическое ожидание: И вот они оба, эти гондона, зациклились. Получается такой порочный круг, ёперный театр. Каждый ждёт того, кто ждёт его.

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

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

DispatchQueue.global().async {
    lockA.lock() // Первый чувак схватил булку (lockA)
    sleep(1) // Пожрал её не спеша, сука
    // Теперь ему масло (lockB) нужно
    lockB.lock() // АХТУНГ! БЛОКИРОВКА! Масло-то второй мудила уже прихватил!
    // Сюда он никогда не попадёт, блядь
    lockB.unlock()
    lockA.unlock()
}

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

Вот и весь сказ, блядь. Два потока, и оба — говна куски, потому что порядок не согласовали.

Как не облажаться? Да ебана в рот, есть же способы! Можно договориться, что все всегда сначала берут lockA, а потом уже lockB. Порядок, блядь, главное! Или использовать try() — типа «эй, мудила, отпустишь? Не? Ну и пошёл ты нахуй, я тогда другим делом займусь».