Почему случается deadlock?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему случается deadlock (взаимная блокировка)?
Deadlock — это ситуация в многопоточном программировании, когда два или более потока (или процесса) бесконечно блокируют друг друга, ожидая освобождения ресурсов, занятых другими участниками блокировки. Это критическая проблема, приводящая к "зависанию" приложения.
Условия возникновения deadlock (Необходимые условия Коффмана)
Для возникновения взаимной блокировки одновременно должны выполняться четыре условия:
-
Условие взаимного исключения (Mutual Exclusion): Ресурс не может быть использован более чем одним потоком одновременно. Если другой поток запрашивает занятый ресурс, он должен ждать его освобождения.
let lock1 = NSLock() let lock2 = NSLock() // Ресурсы, защищенные lock1 и lock2, доступны только одному потоку за раз. -
Условие удержания и ожидания (Hold and Wait): Поток, удерживающий как минимум один ресурс, запрашивает дополнительный ресурс, который в данный момент удерживается другим потоком.
// Поток A: lock1.lock() // Удерживает lock1 // ... выполнение работы ... lock2.lock() // Ждет lock2, который может удерживать Поток B // Поток B: lock2.lock() // Удерживает lock2 // ... выполнение работы ... lock1.lock() // Ждет lock1, который удерживает Поток A -> DEADLOCK -
Условие отсутствия вытеснения (No Preemption): Ресурс может быть освобожден только тем потоком, который его удерживает. Операционная система не может принудительно забрать ресурс у потока.
-
Условие циклического ожидания (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
-
Единый порядок захвата ресурсов (Lock Ordering): Всегда устанавливайте строгий глобальный порядок, в котором потоки должны захватывать ресурсы (например, всегда сначала
lock1, потомlock2). Это нарушает условие циклического ожидания. -
Избегание синхронных вызовов на текущую очередь: Будьте крайне осторожны с
DispatchQueue.sync. Всегда проверяйте, не пытаетесь ли вы вызватьsyncна очереди, которая уже выполняет ваш код. Используйтеasyncили проверяйте текущую очередь сDispatchQueue.assertNotCurrent. -
Использование одномоментной блокировки (Coarse-Grained Locking): Вместо блокировки нескольких мелких ресурсов используйте одну блокировку для всей операции. Это упрощает логику, но может снизить производительность.
-
Отказ от блокировок в пользу безопасных структур данных:
* Используйте **очереди 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), которые минимизируют необходимость ручной работы с блокировками.