Что такое семафор (Semaphore) и как его использовать в iOS для синхронизации?

«Что такое семафор (Semaphore) и как его использовать в iOS для синхронизации?» — вопрос из категории Многопоточность, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Семафор — это примитив синхронизации, который контролирует доступ к общему ресурсу несколькими потоками. Он основан на счетчике, определяющем, сколько потоков могут одновременно войти в критическую секцию. В iOS используется DispatchSemaphore из Grand Central Dispatch (GCD).

Принцип работы:

  1. Семафор инициализируется начальным значением счетчика (например, 1).
  2. Поток вызывает wait(), что уменьшает счетчик. Если счетчик становится отрицательным, поток блокируется до его увеличения.
  3. Поток вызывает signal() после завершения работы с ресурсом, что увеличивает счетчик и может разбудить ожидающий поток.

Пример: Ограничение доступа к общему ресурсу

import Foundation

// Общий ресурс (например, массив)
var sharedArray = [Int]()
let semaphore = DispatchSemaphore(value: 1) // Разрешаем доступ только одному потоку

// Поток 1
DispatchQueue.global().async {
    for i in 1...5 {
        semaphore.wait() // Захватываем семафор (уменьшаем счетчик с 1 до 0)
        sharedArray.append(i)
        print("Поток 1 добавил: (i)")
        semaphore.signal() // Освобождаем семафор (увеличиваем счетчик обратно до 1)
    }
}

// Поток 2
DispatchQueue.global().async {
    for i in 6...10 {
        semaphore.wait() // Будет ждать, если Поток 1 еще в критической секции
        sharedArray.append(i)
        print("Поток 2 добавил: (i)")
        semaphore.signal()
    }
}
// Без семафора вывод мог бы перемешаться, а массив мог бы оказаться в поврежденном состоянии.

Другие сценарии использования:

  • Ограничение количества одновременных сетевых запросов: Инициализировать семафор со значением N (макс. число запросов).
  • Ожидание завершения асинхронных задач: Создать семафор со значением 0 и вызывать signal() по завершению задачи, а основной поток будет ждать через wait().
  • Реализация мьютекса (Mutex): Семафор со значением 1 действует как мьютекс, обеспечивая взаимоисключение (mutual exclusion).

Важные замечания:

  • Взаимоблокировка (Deadlock): Риск возникает, если поток, захвативший семафор, попытается захватить его снова (рекурсивно) без предварительного освобождения. Для рекурсивных блокировок используйте NSRecursiveLock.
  • Приоритет инверсии: Низкоприоритетный поток, захвативший семафор, может блокировать высокоприоритетный.
  • Альтернативы: Для многих случаев синхронизации проще и безопаснее использовать очереди (DispatchQueue) с барьерами (barrier), акторы (Actors) в Swift Concurrency или готовые потокобезопасные коллекции.