> Как сделать потокобезопасным общий массив при синхронных операциях в concurrent очереди (iOS, Swift)

Уровень: senior · Роль: mobile · Категория: Технические вопросы

Компании: Московская биржа

Стек: iOS, Swift

> Пример ответа

Короткий ответ

Потокобезопасность общего массива при синхронных операциях в concurrent очереди достигается через барьеры (barrier). Запись выполняется с флагом .barrier, чтение - обычной синхронной задачей. Это гарантирует эксклюзивный доступ на запись и параллельный доступ на чтение. Альтернативы - отдельная serial очередь, lock или атомарные свойства, но barrier - наиболее эффективный паттерн для read-heavy сценариев.

Подробное объяснение

Concurrent очередь сама по себе не обеспечивает безопасность данных - она лишь управляет порядком выполнения задач. Если несколько потоков одновременно пишут в массив, возникает data race. Решение - использовать DispatchQueue с атрибутом .concurrent и методы sync/async с барьером:

  • Запись: queue.async(flags: .barrier) { array.append(item) } - задача с барьером блокирует очередь, пока не завершится, и не запускается, пока выполняются другие задачи. Это даёт эксклюзивный доступ.
  • Чтение: queue.sync { return array[index] } - обычная задача, может выполняться параллельно с другими чтениями, но не с записью.

Барьер гарантирует: пока идёт запись, ни одно чтение не выполняется, и наоборот. При этом чтения не блокируют друг друга, что даёт выигрыш в производительности по сравнению с serial очередью.

Важно: сам массив должен быть var, а не let, и доступ к нему - только внутри очереди. Никаких прямых обращений извне.

На практике

Типичный пример - потокобезопасная обёртка над массивом для кэша или хранилища данных. Создаётся класс с приватной очередью и приватным массивом, наружу выходят методы read и write. Для синхронных операций используется sync на запись, если нужно вернуть результат, или async с барьером для fire-and-forget.

Для чтения предпочтителен sync, так как он возвращает значение немедленно и не требует обработки completion. Для записи - async с барьером, чтобы не блокировать вызывающий поток, если результат не нужен.

Если операция чтения должна вернуть копию массива - используйте return array внутри sync, это безопасно, так как копирование происходит под защитой очереди.

Пример кода

SWIFT
final class ThreadSafeArray<Element> {
private var array: [Element] = []
private let queue = DispatchQueue(label: "com.example.array", attributes: .concurrent)
func append(_ element: Element) {
queue.async(flags: .barrier) {
self.array.append(element)
}
}
func element(at index: Int) -> Element? {
queue.sync {
guard index >= 0 && index < array.count else { return nil }
return array[index]
}
}
var count: Int {
queue.sync { array.count }
}
func removeAll() {
queue.async(flags: .barrier) {
self.array.removeAll()
}
}
}

Использование:

SWIFT
let safeArray = ThreadSafeArray<Int>()
safeArray.append(1)
safeArray.append(2)
if let value = safeArray.element(at: 0) {
print(value) // 1
}

Как отвечать на собеседовании

Начните с проблемы: concurrent очередь не защищает данные, нужен механизм синхронизации. Затем объясните паттерн barrier: записи - с флагом .barrier, чтения - обычные sync. Подчеркните trade-off: барьер замедляет запись, но ускоряет чтение. Упомяните альтернативы (serial queue, NSLock, атомарные свойства) и объясните, почему barrier предпочтительнее для read-heavy сценариев.

Если спросят про async vs sync - поясните: sync для чтения обязателен, чтобы вернуть значение; для записи можно async, если не нужен результат. Добавьте, что sync на concurrent очереди с барьером не вызывает deadlock, если вызывается не из той же очереди.

Что проверяет интервьюер

  • Понимание разницы между concurrent и serial очередями.
  • Знание механизма barrier и его семантики.
  • Умение проектировать потокобезопасные абстракции.
  • Осознание trade-off между производительностью и безопасностью.
  • Внимание к деталям: приватность массива, отсутствие прямого доступа, корректная обработка границ.

Типичные ошибки

  • Использование sync для записи - блокирует вызывающий поток без необходимости.
  • Забытый .barrier - тогда запись не эксклюзивна, data race остаётся.
  • Прямой доступ к массиву извне очереди - ломает инкапсуляцию.
  • Использование let для массива - нельзя изменить, только заменить, что тоже требует барьера.
  • Вызов sync из той же очереди - deadlock, если очередь serial; для concurrent с barrier - тоже риск.
  • Игнорирование копирования при чтении - если вернуть ссылку на массив, внешний код сможет мутировать его без барьера.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью