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

Как поток может себя заблокировать?

2.7 Senior🔥 151 комментариев
#Многопоточность и асинхронность#Управление памятью

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

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

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

Как поток может заблокировать себя?

В 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-приложений.

Как поток может себя заблокировать? | PrepBro