← Назад к вопросам

Сколько минимально потоков нужно чтобы произошел DeadLock?

2.2 Middle🔥 192 комментариев
#Многопоточность и асинхронность

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Теоретический минимум потоков для Deadlock

Согласно классической теории параллелизма, для возникновения взаимной блокировки (Deadlock) достаточно двух потоков при соблюдении четырех необходимых условий (условий Коффмана). Это фундаментальное положение в компьютерных науках.

Необходимые условия возникновения Deadlock

Для взаимной блокировки должны одновременно выполняться все четыре условия:

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

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

Рассмотрим классический пример с двумя разделяемыми ресурсами (resourceA и resourceB) и двумя потоками (thread1 и thread2), которые пытаются захватить их в разном порядке.

import Foundation

let resourceA = NSLock()
let resourceB = NSLock()

DispatchQueue.global(qos: .background).async {
    // Поток 1: Пытается захватить A, затем B
    resourceA.lock()
    print("Поток 1 захватил ресурс A")
    sleep(1) // Имитация работы, усугубляющая состояние гонки

    resourceB.lock() // БЛОКИРОВКА: Ждет ресурс B, который удерживает Поток 2
    print("Поток 1 захватил ресурс B")

    // Критическая секция (никогда не будет достигнута)
    resourceB.unlock()
    resourceA.unlock()
}

DispatchQueue.global(qos: .utility).async {
    // Поток 2: Пытается захватить B, затем A (ОПАСНЫЙ ОБРАТНЫЙ ПОРЯДОК!)
    resourceB.lock()
    print("Поток 2 захватил ресурс B")
    sleep(1)

    resourceA.lock() // БЛОКИРОВКА: Ждет ресурс A, который удерживает Поток 1
    print("Поток 2 захватил ресурс A")

    // Критическая секция (никогда не будет достигнута)
    resourceA.unlock()
    resourceB.unlock()
}

// Даем время на выполнение
sleep(5)
print("Программа завершена (эта строка будет выведена, но потоки зависли)")

Пошаговый анализ сценария deadlock:

  1. T1 захватывает resourceA.
  2. T2 захватывает resourceB.
  3. T1 пытается захватить resourceB и блокируется, ожидая его освобождения T2.
  4. T2 пытается захватить resourceA и блокируется, ожидая его освобождения T1.
  5. Возникает циклическое ожидание: T1 → B (удерживается T2) → T2 → A (удерживается T1). Все четыре условия соблюдены, программа на iOS зависает в части потоков.

Практические аспекты в iOS-разработке

Хотя теоретически достаточно двух потоков, на практике в современных сложных iOS-приложениях deadlock может возникать в более запутанных конфигурациях:

  • Основная очередь (Main Thread) и серийные очереди: Распространенный сценарий — выполнение синхронной задачи (sync) на текущей очереди.
    // ГАРАНТИРОВАННЫЙ DEADLOCK на главном потоке
    DispatchQueue.main.sync {
        // Этот код никогда не выполнится
    }
    
    Здесь тот же поток (главный) пытается ждать завершения задачи, которую сам же должен выполнить, что нарушает логику последовательности. Это частный случай, но он также соответствует условиям блокировки для одного потока, выполняющего роль обоих участников цикла.

  • Глобальные очереди и пользовательские серийные очереди: Взаимные блокировки могут возникать между несколькими серийными очередями при захвате ресурсов в разном порядке, как в примере выше, но с использованием DispatchQueue и семафоров.

  • OperationQueue с зависимостями: Неправильно настроенные зависимости между Operation могут создать циклическую цепочку ожидания.

    let operationA = BlockOperation { /* ... */ }
    let operationB = BlockOperation { /* ... */ }
    
    operationA.addDependency(operationB)
    operationB.addDependency(operationA) // ЦИКЛИЧЕСКАЯ ЗАВИСИМОСТЬ -> DEADLOCK!
    
    let queue = OperationQueue()
    queue.addOperations([operationA, operationB], waitUntilFinished: false)
    

Профилактика Deadlock в iOS

Чтобы избежать взаимных блокировок, следует нарушать хотя бы одно из четырех условий:

  • Строгий порядок захвата ресурсов (Нарушает циклическое ожидание): Всегда устанавливайте глобальный порядок блокировки ресурсов (например, сначала всегда resourceA, затем resourceB).
  • Использование атомарных операций и actor (Swift 5.5+) (Ослабляет условие взаимного исключения через управляемый доступ): Actor избавляет от необходимости вручную захватывать блокировки.
    actor BankAccount {
        private var balance: Double = 0
        
        func deposit(_ amount: Double) {
            balance += amount
        }
    }
    // Компилятор гарантирует эксклюзивный доступ, не требуя ручных locks.
    
  • Использование async/await вместо ручных блокировок: Уменьшает количество точек явного ожидания (sync, wait).
  • Отказ от sync вызовов на текущей очереди: Всегда проверяйте контекст выполнения.
  • Использование NSLock с таймаутом (lock(before:)) или os_unfair_lock: Позволяет реализовать логику восстановления после неудачной попытки захвата.
  • Тщательное проектирование зависимостей в OperationQueue: Избегайте циклов в графе зависимостей.

Итог: Хотя для образования классического deadlock минимально необходимо два потока (или две серийные очереди), в реальной iOS-разработке ключевую роль играет не количество участников, а архитектура доступа к разделяемым ресурсам и выполнение всех четырех условий Коффмана. Основная мера защиты — строгий контроль порядка синхронизации и использование современных абстракций Swift Concurrency (actor, await).

Сколько минимально потоков нужно чтобы произошел DeadLock? | PrepBro