Какие основные способы избежать взаимной блокировки (deadlock) в iOS?

Ответ

Deadlock (взаимная блокировка) возникает, когда два или более потока бесконечно ожидают ресурсы, захваченные друг другом. Основные стратегии предотвращения в iOS/ Swift:

1. Единый порядок захвата блокировок Всегда захватывайте мьютексы (NSLock, os_unfair_lock) в строго одинаковом порядке во всей программе.

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

// ПРАВИЛЬНО: Всегда сначала A, потом B
func safeAccess() {
    lockA.lock()
    defer { lockA.unlock() }

    lockB.lock()
    defer { lockB.unlock() }

    // Критическая секция
}

// НЕПРАВИЛЬНО: Разный порядок в разных функциях ведёт к риску deadlock.

2. Использование блокировок с таймаутом Вместо бесконечного ожидания используйте попытку захвата с ограничением по времени.

if lockA.lock(before: Date().addingTimeInterval(0.5)) {
    defer { lockA.unlock() }
    // Успешный захват
} else {
    // Таймаут — принимаем решение (например, откат операции)
    print("Не удалось захватить блокировку, избегаем deadlock")
}

3. Предпочтение серийных очередей GCD мьютексам Серийная очередь (DispatchQueue) по своей природе исключает deadlock для задач в ней, если не вызывать sync из той же очереди.

let serialQueue = DispatchQueue(label: "com.example.serial")

serialQueue.async {
    // Эта критическая секция никогда не выполнится параллельно
    // с другим блоком на этой же очереди.
}

// ОПАСНО: Вызов sync из той же очереди вызовет deadlock.
serialQueue.sync { /* Этот код никогда не выполнится */ }

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

5. Использование высокоуровневых примитивов

  • DispatchGroup для координации групп задач.
  • NSOperationQueue с настройкой зависимостей (addDependency).
  • Актёры (Actors) в Swift (доступно с Swift 5.5) для изоляции состояния.
// Пример с Actor (Swift 5.5+)
actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }
}
// Компилятор гарантирует эксклюзивный доступ к свойствам актёра.

Ответ 18+ 🔞

А, слушай, смотри, вот эта хуйня — deadlock, или, по-нашему, взаимная блокировка. Представь себе двух упырей, которые схватили друг друга за горло и оба ждут, пока первый разожмёт руку. И так до скончания века, пока система не накроется медным тазом. В iOS, на Swift, это как два потока устроили друг другу вечную пиздюлину за ресурсы. Чтобы такого не было, вот тебе стратегии, как не наступить на эти грабли.

1. Порядок — всё, блядь! Если ты хватаешь несколько мьютексов (типа NSLock или os_unfair_lock), делай это всегда в одном и том же, блядь, порядке. По всей программе! Не выёбывайся.

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

// ПРАВИЛЬНО: Сначала всегда А, потом Б. Как в армии — старший по званию первый.
func safeAccess() {
    lockA.lock()
    defer { lockA.unlock() }

    lockB.lock()
    defer { lockB.unlock() }

    // Вот тут твоя критическая секция, делай что хочешь
}

// НЕПРАВИЛЬНО: Если в другой функции сначала Б, а потом А — готовься к deadlock. Это как пытаться засунуть хуй в бутылку, начиная с горлышка и с донышка одновременно.

2. Не жди вечно, поставь таймер Вместо того чтобы тупо висеть на lock(), используй попытку захвата с таймаутом. Если не получилось — отъебнись и прими решение.

if lockA.lock(before: Date().addingTimeInterval(0.5)) {
    defer { lockA.unlock() }
    // Ура, захватил, молодец
} else {
    // Таймаут, ёпта. Не судьба. Откатывай операцию, логируй, но не стой столбом.
    print("Не удалось захватить блокировку, избегаем deadlock")
}

3. GCD — твой друг, если не долбаёб Серийная очередь (DispatchQueue) — это святое. Она по своей природе не даёт deadlock, если ты, конечно, не начнёшь вызывать sync из неё же самой. Это как пытаться самому себя выебать — неебически сложно и бессмысленно.

let serialQueue = DispatchQueue(label: "com.example.serial")

serialQueue.async {
    // Эта штука никогда не будет бегать параллельно с другим блоком в этой очереди. Всё чинно, благородно.
}

// ОПАСНО, БЛЯДЬ! Вызов sync из той же очереди — гарантированный deadlock. Код ниже никогда не выполнится, и ты будешь чесать репу.
serialQueue.sync { /* Этот код никогда не выполнится */ }

4. Не усложняй, блядь Старайся не хватать вторую блокировку, когда уже держишь первую. Это как пытаться нести два арбуза под мышкой и ещё открыть дверь. Проектируй код так, чтобы одной блокировки хватало, или пересматривай архитектуру, ёпта.

5. Используй крутые высокоуровневые штуки Зачем изобретать велосипед, когда есть готовые тачки?

  • DispatchGroup — чтобы координировать кучу задач.
  • NSOperationQueue с зависимостями (addDependency) — говоришь, что делать после чего.
  • Актёры (Actors) в Swift (с версии 5.5) — это вообще охуенно, компилятор сам следит, чтобы к данным был эксклюзивный доступ.
// Пример с Actor (Swift 5.5+)
actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }
}
// Компилятор сам, блядь, гарантирует, что к `balance` не будет гонок. Красота!

Вот и всё, ебать мои старые костыли. Следуй этим правилам, и твой код не повиснет в вечной борьбе двух упырей за ресурсы. А если повиснет — ну, значит, где-то накосячил, ищи, где порядок нарушил или sync не там вызвал. Удачи, ёпта!