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

Почему случается deadlock?

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

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

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

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

Почему случается deadlock (взаимная блокировка)?

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

Условия возникновения deadlock (Необходимые условия Коффмана)

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

  1. Условие взаимного исключения (Mutual Exclusion): Ресурс не может быть использован более чем одним потоком одновременно. Если другой поток запрашивает занятый ресурс, он должен ждать его освобождения.

    let lock1 = NSLock()
    let lock2 = NSLock()
    
    // Ресурсы, защищенные lock1 и lock2, доступны только одному потоку за раз.
    
  2. Условие удержания и ожидания (Hold and Wait): Поток, удерживающий как минимум один ресурс, запрашивает дополнительный ресурс, который в данный момент удерживается другим потоком.

    // Поток A:
    lock1.lock() // Удерживает lock1
    // ... выполнение работы ...
    lock2.lock() // Ждет lock2, который может удерживать Поток B
    
    // Поток B:
    lock2.lock() // Удерживает lock2
    // ... выполнение работы ...
    lock1.lock() // Ждет lock1, который удерживает Поток A -> DEADLOCK
    
  3. Условие отсутствия вытеснения (No Preemption): Ресурс может быть освобожден только тем потоком, который его удерживает. Операционная система не может принудительно забрать ресурс у потока.

  4. Условие циклического ожидания (Circular Wait): Существует кольцевая цепочка потоков, в которой каждый поток ждет ресурс, удерживаемый следующим потоком в цепочке.

    Поток A удерживает Ресурс 1 и ждет Ресурс 2.
    Поток B удерживает Ресурс 2 и ждет Ресурс 1.
    // Образуется цикл A -> B -> A...
    

Если нарушить хотя бы одно из этих условий, deadlock станет невозможным.

Типичные сценарии deadlock в iOS-разработке

1. Неправильная последовательность блокировок

Самая частая причина. Потоки захватывают мьютексы (NSLock, os_unfair_lock, pthread_mutex) или семафоры (DispatchSemaphore) в разном порядке.

// НЕПРАВИЛЬНО: Разный порядок захвата -> риск deadlock
func threadA() {
    lock1.lock()
    lock2.lock() // Если threadB уже захватил lock2, мы ждем...
    // Критическая секция
    lock2.unlock()
    lock1.unlock()
}

func threadB() {
    lock2.lock()
    lock1.lock() // А здесь ждем lock1 от threadA -> DEADLOCK
    // Критическая секция
    lock1.unlock()
    lock2.unlock()
}

2. Синхронные вызовы (sync) на той же очереди в GCD

Grand Central Dispatch — мощный инструмент, но sync вызов на текущей serial очереди гарантированно приводит к deadlock.

let mainQueue = DispatchQueue.main
mainQueue.async {
    // Асинхронная задача на Main queue
    mainQueue.sync { // SYNCHRONOUS вызов на ТЕ ЖЕ САМЫЕ queue!
        // Этот блок никогда не выполнится.
        // Внешний async блок ждет завершения sync, но sync
        // не может начаться, пока не завершится внешний async.
        print("Этот код недостижим из-за deadlock.")
    }
}

3. Взаимоблокировки с участием @synchronized в Objective-C или Swift

@synchronized неявно создает блокировки на объектах. Вложенная синхронизация на разные объекты в разном порядке создает тот же риск.

// Objective-C
- (void)methodA {
    @synchronized(object1) {
        @synchronized(object2) { ... }
    }
}

- (void)methodB {
    @synchronized(object2) {
        @synchronized(object1) { ... } // Потенциальный deadlock!
    }
}

4. Блокировки в комбинации с операциями Operation и зависимостями

Создание циклических зависимостей между Operation в OperationQueue.

let operationA = BlockOperation { /* ... */ }
let operationB = BlockOperation { /* ... */ }

operationA.addDependency(operationB)
operationB.addDependency(operationA) // Цикл! Ни одна операция не сможет начаться.

let queue = OperationQueue()
queue.addOperations([operationA, operationB], waitUntilFinished: false)

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

  1. Единый порядок захвата ресурсов (Lock Ordering): Всегда устанавливайте строгий глобальный порядок, в котором потоки должны захватывать ресурсы (например, всегда сначала lock1, потом lock2). Это нарушает условие циклического ожидания.

  2. Избегание синхронных вызовов на текущую очередь: Будьте крайне осторожны с DispatchQueue.sync. Всегда проверяйте, не пытаетесь ли вы вызвать sync на очереди, которая уже выполняет ваш код. Используйте async или проверяйте текущую очередь с DispatchQueue.assertNotCurrent.

  3. Использование одномоментной блокировки (Coarse-Grained Locking): Вместо блокировки нескольких мелких ресурсов используйте одну блокировку для всей операции. Это упрощает логику, но может снизить производительность.

  4. Отказ от блокировок в пользу безопасных структур данных:

    *   Используйте **очереди GCD** (`DispatchQueue`) с барьерами (`barrier`) для чтения/записи.
    *   Применяйте **акторную модель (Actors)**, представленную в Swift 5.5. Акторы изолируют свои данные и обрабатывают запросы последовательно, что исключает классические deadlock'и для изолированного состояния.
```swift
// Swift Actor предотвращает гонки данных и deadlock для своего состояния.
actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }
}
```

5. Инструменты обнаружения: В сложных системах используйте статический анализ кода, динамические детекторы (например, Thread Sanitizer в Xcode) и тщательное проектирование.

Итог: Deadlock возникает из-за ошибок проектирования параллельного доступа к общим ресурсам. Ключ к предотвращению — строгая дисциплина при работе с примитивами синхронизации, понимание условий Коффмана и активное использование современных высокоуровневых абстракций Swift (акторы, async/await), которые минимизируют необходимость ручной работы с блокировками.