Как поток может себя заблокировать?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как поток может заблокировать себя?
В iOS-разработке, работая с многопоточностью, мы часто сталкиваемся с ситуациями, когда поток может сам себя заблокировать. Это нежелательное состояние, когда поток ожидает условие, которое никогда не наступит из-за его собственных действий. Рассмотрим основные механизмы и примеры.
Основные причины самоблокировки
- Deadlock (Взаимная блокировка) в одном потоке
* Чаще всего возникает при неправильном использовании **мьютексов (NSLock, pthread_mutex)** или **семафоров (dispatch_semaphore_t)** в рекурсивных вызовах или при попытке захватить один и тот же ресурс дважды в нерекурсивном режиме.
- Некорректное использование GCD (Grand Central Dispatch)
* Например, вызов `dispatch_sync` на текущей очереди.
- Блокирующие операции в основном потоке
* Хотя это не "самблокировка" в классическом deadlock–смысле, но поток UI блокирует себя от обработки событий, что приводит к "зависанию" интерфейса.
Примеры кода и разбор
1. Deadlock с нерекурсивным мьютексом
import Foundation
let lock = NSLock()
func criticalSection() {
lock.lock()
// Попытка захватить тот же мьютекс ВНУТРИ уже захваченной секции
lock.lock() // Поток БЛОКИРУЕТСЯ здесь навсегда, ожидая самого себя
print("Этот код никогда не выполнится")
lock.unlock()
lock.unlock()
}
// Вызов функции приведет к вечной блокировке потока
// criticalSection()
Почему это происходит? NSLock по умолчанию — нерекурсивный. Если поток, который уже владеет замком, попытается захватить его снова, он будет ждать его освобождения бесконечно, но освободить его может только он сам. Решение — использовать NSRecursiveLock для рекурсивных структур.
2. Классический dispatch_sync на текущей очереди
Это, пожалуй, самый частый и коварный пример в iOS.
import Dispatch
let mainQueue = DispatchQueue.main
// Выполняем этот код на main queue (например, в viewDidLoad)
mainQueue.async {
// ОПАСНО: синхронный вызов на той же очереди
mainQueue.sync {
print("Эта задача никогда не начнется")
}
print("Эта строка также никогда не выполнится")
}
Механизм блокировки: Функция dispatch_sync добавляет задачу в очередь и блокирует текущий поток до её завершения. Однако, если эта задача добавлена в ту же самую очередь, она никогда не сможет начать выполнение, так как очередь занята ожидающим потоком. Образуется цикл: очередь ждет завершения задачи, но задача не может стартовать, потому что очередь не продвигается.
3. Неправильная работа с семафорами
import Dispatch
let semaphore = DispatchSemaphore(value:رو) // Изначально 0 ресурсов
func taskA() {
semaphore.wait() // Запрашиваем ресурс. Их 0 -> поток БЛОКИРУЕТСЯ.
// Чтобы разблокироваться, нужно вызвать signal()...
semaphore.signal() // ... но до этой строки выполнение никогда не дойдет.
}
taskA() // Поток навсегда зависнет на wait()
Проблема: Семафор инициализирован с нулевым значением. Первый же вызов wait() блокирует поток, ожидая появления ресурса. Поскольку поток заблокирован, он не может выполнить код, который бы вызвал signal() и увеличил счетчик.
4. Неявная блокировка через completion handler
Рассмотрим более сложный асинхронный сценарий:
import Dispatch
let serialQueue = DispatchQueue(label: "com.example.serial")
var isReady = false
let conditionLock = NSConditionLock(condition: 0)
serialQueue.async {
conditionLock.lock(whenCondition: 1) // Ждем, когда isReady станет true
print("Condition met")
conditionLock.unlock()
}
// Теперь на ТОЙ ЖЕ САМОЙ очереди пытаемся выполнить код,
// который должен выполнить условие
serialQueue.async {
// Этот блок не начнет выполняться, пока предыдущий async не завершится.
// Но предыдущий блок ждет изменения condition, которое должно произойти здесь.
conditionLock.lock()
isReady = true
conditionLock.unlock(withCondition: 1) // Сигналим об изменении условия
}
Здесь два блока, отправленные в серийную (serial) очередь, не могут выполняться параллельно. Первый блок ждет сигнала от второго, но второй блок ждет, пока первый освободит очередь, на которой они оба находятся.
Как избежать самоблокировки? Краткие рекомендации:
- Для рекурсивных вызовов всегда используйте
NSRecursiveLockили рекурсивные мьютексыpthread_mutexattr_settypeсPTHREAD_MUTEX_RECURSIVE. - Никогда не вызывайте
dispatch_syncна текущей очереди. Всегда проверяйте, не находитесь ли вы уже на целевой очереди (можно использоватьDispatchQueue.setSpecificиDispatchQueue.getSpecificдля меток). - Внимательно проектируйте порядок захвата ресурсов. Избегайте ситуаций, когда один поток может попытаться захватить ресурс A, а затем B, в то время как другой поток делает это в обратном порядке (B -> A). Это классический deadlock даже между потоками.
- Используйте асинхронные вызовы (
dispatch_async) вместо синхронных, где это возможно, особенно при работе с основным потоком из фоновых задач. - Отлаживайте подозрительные блокировки с помощью Instruments (инструмент "System Trace") или добавляя логи с идентификаторами потоков (
Thread.current).
Понимание этих механизмов критически важно для создания стабильных, отзывчивых и бездедлокных iOS-приложений.